···11+# Chapter 1: Why Functional TypeScript?
22+33+TypeScript gives you a powerful type system. You define interfaces, catch type errors at compile time, and get autocomplete in your editor. It feels like you're in control.
44+55+Then you write `try/catch` and it all falls apart.
66+77+```typescript
88+try {
99+ const user = await fetchUser(id)
1010+ const orders = await fetchOrders(user.id)
1111+ return processOrders(orders)
1212+} catch (e) {
1313+ // What is e? Network error? Not found? Parse error? Server error?
1414+ // TypeScript shrugs: "It's unknown. Good luck."
1515+}
1616+```
1717+1818+This chapter explains why exceptions break TypeScript's type safety and how purus fixes it.
1919+2020+---
2121+2222+## The Problem: Exceptions Are Invisible
2323+2424+Consider a simple function:
2525+2626+```typescript
2727+const divide = (a: number, b: number): number => {
2828+ if (b === 0) throw new Error("Division by zero")
2929+ return a / b
3030+}
3131+```
3232+3333+The return type says `number`. But that's a lie - this function can also throw. The type signature doesn't mention it. TypeScript doesn't track it. Your IDE won't warn you.
3434+3535+Now imagine this in a real codebase:
3636+3737+```typescript
3838+const processOrder = async (orderId: string): Promise<Order> => {
3939+ const order = await db.findOrder(orderId) // Throws if not found
4040+ const user = await db.findUser(order.userId) // Throws if not found
4141+ await payment.charge(user, order.total) // Throws if declined
4242+ await inventory.reserve(order.items) // Throws if out of stock
4343+ await email.send(user.email, "Order confirmed") // Throws if invalid email
4444+ return order
4545+}
4646+```
4747+4848+How many things can fail? Five. What are the errors? Unknown. The type signature says `Promise<Order>` but the reality is:
4949+5050+```
5151+Promise<Order | NetworkError | NotFoundError | PaymentError | InventoryError | EmailError>
5252+```
5353+5454+TypeScript can't express this. So errors become invisible. They bubble up through your call stack, get caught somewhere (maybe), and by then you've lost all context about what actually failed.
5555+5656+---
5757+5858+## The Solution: Errors as Values
5959+6060+What if errors were part of the return type?
6161+6262+```typescript
6363+const divide = (a: number, b: number): Result<number, DivisionError> => {
6464+ if (b === 0) return err({ _tag: "DivisionByZero" })
6565+ return ok(a / b)
6666+}
6767+```
6868+6969+Now the type tells the truth. This function returns either:
7070+- `Ok<number>` - the division succeeded
7171+- `Err<DivisionError>` - division by zero
7272+7373+The caller must handle both cases:
7474+7575+```typescript
7676+const result = divide(10, 0)
7777+7878+if (result._tag === "Ok") {
7979+ console.log(`Result: ${result.value}`)
8080+} else {
8181+ console.log(`Error: ${result.error._tag}`)
8282+}
8383+```
8484+8585+No try/catch. No invisible failure modes. The type system tracks everything.
8686+8787+---
8888+8989+## Composition: The pipe() Pattern
9090+9191+Handling Result manually gets verbose. Consider:
9292+9393+```typescript
9494+const result1 = divide(10, 2)
9595+if (result1._tag === "Err") return result1
9696+9797+const result2 = divide(result1.value, 3)
9898+if (result2._tag === "Err") return result2
9999+100100+return ok(result2.value * 2)
101101+```
102102+103103+purus provides `pipe()` and combinators that handle this:
104104+105105+```typescript
106106+import { pipe, ok, mapResult, chainResult } from "purus-ts"
107107+108108+const result = pipe(
109109+ divide(10, 2),
110110+ chainResult(n => divide(n, 3)),
111111+ mapResult(n => n * 2)
112112+)
113113+```
114114+115115+If any step fails, the error propagates automatically. If all succeed, you get the final value. The code reads top-to-bottom, no nesting, no early returns.
116116+117117+This is the core insight: **errors flow through the pipeline just like success values**. You compose operations, and error handling comes for free.
118118+119119+---
120120+121121+## What purus Provides
122122+123123+purus is a minimal library (~950 lines) that gives you:
124124+125125+### Errors as Values
126126+- **Result<T, E>** - Success or failure with typed error
127127+- **Option<T>** - Value or nothing (like nullable, but composable)
128128+129129+### Pattern Matching
130130+- **match()** - Exhaustive matching (forget a case = compile error)
131131+- **matchResult()**, **matchOption()** - Specialized matchers
132132+133133+### Type-Safe Primitives
134134+- **Branded<T, B>** - Distinct types from primitives (UserId vs OrderId)
135135+- **Refined<T, R>** - Types with validation (PositiveNumber, Email)
136136+- **Entity<T, S>** - Typestate for state machines
137137+138138+### Effect System
139139+- **Eff<A, E, R>** - Lazy, composable, typed async operations
140140+- **Combinators** - timeout, retry, race, all
141141+- **Dependency Injection** - provide(), access() without frameworks
142142+143143+### Concurrency
144144+- **Fibers** - Lightweight threads with cancellation
145145+- **Real cleanup** - Unlike Promise.race, cancelled work actually stops
146146+147147+---
148148+149149+## Your First Example
150150+151151+Let's see the difference in practice. Here's typical TypeScript:
152152+153153+```typescript
154154+// Vanilla TypeScript
155155+const fetchUser = async (id: string): Promise<User> => {
156156+ const response = await fetch(`/users/${id}`)
157157+ if (!response.ok) {
158158+ throw new Error(`HTTP ${response.status}`)
159159+ }
160160+ return response.json()
161161+}
162162+163163+// Caller has no idea what can fail
164164+try {
165165+ const user = await fetchUser("123")
166166+ console.log(user.name)
167167+} catch (e) {
168168+ // e is unknown. Network error? 404? 500? JSON parse error?
169169+ console.error("Something went wrong:", e)
170170+}
171171+```
172172+173173+Here's the same thing with purus:
174174+175175+```typescript
176176+// With purus
177177+import { type Result, ok, err, matchResult } from "purus-ts"
178178+179179+type FetchError =
180180+ | { _tag: "NetworkError"; message: string }
181181+ | { _tag: "NotFound"; id: string }
182182+ | { _tag: "ServerError"; status: number }
183183+184184+const fetchUser = async (id: string): Promise<Result<User, FetchError>> => {
185185+ try {
186186+ const response = await fetch(`/users/${id}`)
187187+ if (response.status === 404) {
188188+ return err({ _tag: "NotFound", id })
189189+ }
190190+ if (!response.ok) {
191191+ return err({ _tag: "ServerError", status: response.status })
192192+ }
193193+ return ok(await response.json())
194194+ } catch (e) {
195195+ return err({ _tag: "NetworkError", message: String(e) })
196196+ }
197197+}
198198+199199+// Caller knows exactly what can fail
200200+const result = await fetchUser("123")
201201+202202+matchResult(
203203+ user => console.log(user.name),
204204+ error => {
205205+ // TypeScript knows error is FetchError
206206+ // match() forces us to handle all cases
207207+ switch (error._tag) {
208208+ case "NetworkError": console.error("Network:", error.message); break
209209+ case "NotFound": console.error("User not found:", error.id); break
210210+ case "ServerError": console.error("Server error:", error.status); break
211211+ }
212212+ }
213213+)(result)
214214+```
215215+216216+The purus version is more code upfront, but:
217217+218218+1. **The type tells the truth** - `Result<User, FetchError>` is honest
219219+2. **Errors are structured** - Each has a `_tag` and relevant data
220220+3. **Handling is exhaustive** - Add a new error type, compiler reminds you
221221+4. **No surprises** - You can't forget to handle failure
222222+223223+---
224224+225225+## When to Use This Approach
226226+227227+Errors as values shine when:
228228+229229+- **Multiple things can fail** - You need to know which one
230230+- **Errors need context** - NotFound needs the ID, Validation needs the field
231231+- **Recovery is possible** - You want to try fallbacks or retry
232232+- **Testing matters** - No mocking throw behavior, just return values
233233+234234+Traditional exceptions are fine for:
235235+236236+- **Truly exceptional cases** - Out of memory, stack overflow
237237+- **Programmer errors** - Assertions, invariant violations
238238+- **Quick scripts** - When you don't care about error handling
239239+240240+---
241241+242242+## What's Next
243243+244244+In the next chapter, you'll learn about the Effect type - purus's core abstraction for async operations. Effects take the ideas from this chapter and make them work with async code, cancellation, and dependency injection.
245245+246246+[Continue to Chapter 2: Your First Effect →](./02-your-first-effect.md)