···11+# Why Errors as Values?
22+33+This article explains the fundamental shift from throwing exceptions to returning typed errors, and why this change makes your TypeScript code more reliable.
44+55+---
66+77+## The Exception Problem
88+99+TypeScript has a powerful type system. You define interfaces, use generics, and the compiler catches type errors before your code runs. It's a safety net that prevents entire categories of bugs.
1010+1111+Except for exceptions.
1212+1313+```typescript
1414+const fetchUser = async (id: string): Promise<User> => {
1515+ const response = await fetch(`/users/${id}`)
1616+ if (!response.ok) {
1717+ throw new Error(`HTTP ${response.status}`)
1818+ }
1919+ return response.json()
2020+}
2121+```
2222+2323+The return type says `Promise<User>`. But this function can also throw. What can it throw? The type system doesn't say. Could be:
2424+- Network error (fetch failed)
2525+- HTTP error (we throw it explicitly)
2626+- JSON parse error (response.json() failed)
2727+- Something else entirely
2828+2929+Now look at the caller:
3030+3131+```typescript
3232+try {
3333+ const user = await fetchUser("123")
3434+ console.log(user.name)
3535+} catch (e) {
3636+ // What is e?
3737+}
3838+```
3939+4040+The variable `e` has type `unknown`. TypeScript has given up. All the careful type work you did is gone the moment an exception is thrown.
4141+4242+### The Real Cost
4343+4444+This isn't just an academic problem. In real codebases:
4545+4646+1. **Errors are invisible** - Function signatures don't mention what can fail
4747+2. **Handling is guesswork** - You catch `unknown` and hope for the best
4848+3. **Refactoring is dangerous** - Change what a function throws, callers don't know
4949+4. **Testing is incomplete** - Hard to verify you handle all error cases
5050+5151+Consider a chain of function calls:
5252+5353+```typescript
5454+const processOrder = async (orderId: string) => {
5555+ const order = await fetchOrder(orderId) // Can throw A, B
5656+ const user = await fetchUser(order.userId) // Can throw C, D
5757+ await chargePayment(user, order.total) // Can throw E, F, G
5858+ await sendEmail(user.email, "Confirmed") // Can throw H
5959+ return order
6060+}
6161+```
6262+6363+What can `processOrder` throw? Everything from A to H. But the type signature just says `Promise<Order>`. Anyone calling this function has no idea what might go wrong.
6464+6565+---
6666+6767+## Railway Oriented Programming
6868+6969+Imagine your code as a railway track. The happy path is one track - data flows through transformations until you reach the destination.
7070+7171+```
7272+[Input] → [Transform A] → [Transform B] → [Transform C] → [Output]
7373+```
7474+7575+When something fails, you need to switch tracks:
7676+7777+```
7878+[Input] → [Transform A] → [Transform B] ─┬→ [Transform C] → [Output]
7979+ │
8080+ └→ [Error Track] → [Error Output]
8181+```
8282+8383+This is **Railway Oriented Programming**. Instead of throwing to escape the happy path, you return a value that can be either success or failure. The type system tracks both tracks.
8484+8585+In purus, this is the `Result<T, E>` type:
8686+8787+```typescript
8888+type Result<T, E> =
8989+ | { _tag: "Ok"; value: T } // Success track
9090+ | { _tag: "Err"; error: E } // Error track
9191+```
9292+9393+Every function that can fail returns `Result`. The caller sees both possibilities in the type.
9494+9595+---
9696+9797+## The Result Pattern
9898+9999+### Creating Results
100100+101101+```typescript
102102+import { ok, err } from "purus-ts"
103103+104104+// Success
105105+const success = ok(42)
106106+// Type: Ok<number>
107107+108108+// Failure
109109+const failure = err({ _tag: "NotFound", id: "123" })
110110+// Type: Err<{ _tag: "NotFound"; id: string }>
111111+```
112112+113113+### Checking Results
114114+115115+```typescript
116116+import { isOk, isErr } from "purus-ts"
117117+118118+const result: Result<number, MyError> = someFunction()
119119+120120+if (isOk(result)) {
121121+ console.log(result.value) // TypeScript knows it's Ok
122122+} else {
123123+ console.log(result.error) // TypeScript knows it's Err
124124+}
125125+```
126126+127127+### Pattern Matching
128128+129129+The `matchResult` function ensures you handle both cases:
130130+131131+```typescript
132132+import { matchResult } from "purus-ts"
133133+134134+const message = matchResult(
135135+ value => `Success: ${value}`,
136136+ error => `Error: ${error._tag}`
137137+)(result)
138138+```
139139+140140+If you forget to handle one case, TypeScript complains.
141141+142142+---
143143+144144+## Error Composition
145145+146146+The real power of Result is composition. Errors flow through your pipeline automatically.
147147+148148+### mapResult - Transform success values
149149+150150+```typescript
151151+import { ok, mapResult, pipe } from "purus-ts"
152152+153153+const result = pipe(
154154+ ok(10),
155155+ mapResult(n => n * 2),
156156+ mapResult(n => n.toString())
157157+)
158158+// Result: Ok("20")
159159+```
160160+161161+If the input is `Err`, `mapResult` does nothing - the error passes through unchanged.
162162+163163+### chainResult - Chain operations that can fail
164164+165165+```typescript
166166+import { ok, err, chainResult, pipe } from "purus-ts"
167167+168168+const divide = (a: number, b: number): Result<number, string> =>
169169+ b === 0 ? err("Division by zero") : ok(a / b)
170170+171171+const result = pipe(
172172+ ok(100),
173173+ chainResult(n => divide(n, 5)), // Ok(20)
174174+ chainResult(n => divide(n, 0)), // Err("Division by zero")
175175+ chainResult(n => divide(n, 2)) // Never runs - already Err
176176+)
177177+// Result: Err("Division by zero")
178178+```
179179+180180+Once you're on the error track, you stay there. No more transformations run.
181181+182182+### unwrapOr - Get the value with a default
183183+184184+```typescript
185185+import { ok, err, unwrapOr, pipe } from "purus-ts"
186186+187187+const value1 = pipe(ok(42), unwrapOr(0)) // 42
188188+const value2 = pipe(err("oops"), unwrapOr(0)) // 0
189189+```
190190+191191+### mapErr - Transform error values
192192+193193+```typescript
194194+import { err, mapErr, pipe } from "purus-ts"
195195+196196+const result = pipe(
197197+ err("raw error"),
198198+ mapErr(msg => ({ _tag: "FormattedError" as const, message: msg }))
199199+)
200200+// Result: Err({ _tag: "FormattedError", message: "raw error" })
201201+```
202202+203203+Useful for converting between error types at module boundaries.
204204+205205+---
206206+207207+## Real-World Example: API Client
208208+209209+Let's build a typed API client that makes errors explicit.
210210+211211+### Define Error Types
212212+213213+```typescript
214214+type ApiError =
215215+ | { _tag: "NetworkError"; message: string }
216216+ | { _tag: "NotFound"; resource: string; id: string }
217217+ | { _tag: "Unauthorized" }
218218+ | { _tag: "ValidationError"; fields: string[] }
219219+ | { _tag: "ServerError"; status: number }
220220+```
221221+222222+Each error variant has a `_tag` for pattern matching and relevant data for debugging.
223223+224224+### Create the Fetch Wrapper
225225+226226+```typescript
227227+import { type Result, ok, err } from "purus-ts"
228228+229229+const apiFetch = async <T>(
230230+ url: string
231231+): Promise<Result<T, ApiError>> => {
232232+ try {
233233+ const response = await fetch(url)
234234+235235+ if (response.status === 401) {
236236+ return err({ _tag: "Unauthorized" })
237237+ }
238238+239239+ if (response.status === 404) {
240240+ return err({ _tag: "NotFound", resource: url, id: "" })
241241+ }
242242+243243+ if (response.status === 422) {
244244+ const body = await response.json()
245245+ return err({ _tag: "ValidationError", fields: body.fields || [] })
246246+ }
247247+248248+ if (!response.ok) {
249249+ return err({ _tag: "ServerError", status: response.status })
250250+ }
251251+252252+ const data = await response.json()
253253+ return ok(data as T)
254254+255255+ } catch (e) {
256256+ return err({
257257+ _tag: "NetworkError",
258258+ message: e instanceof Error ? e.message : "Unknown error"
259259+ })
260260+ }
261261+}
262262+```
263263+264264+### Use It
265265+266266+```typescript
267267+import { matchResult, pipe, chainResult, mapResult } from "purus-ts"
268268+269269+type User = { id: string; name: string; email: string }
270270+type Order = { id: string; userId: string; total: number }
271271+272272+const getUser = (id: string) => apiFetch<User>(`/users/${id}`)
273273+const getOrders = (userId: string) => apiFetch<Order[]>(`/users/${userId}/orders`)
274274+275275+const getUserWithOrders = async (userId: string) => {
276276+ const userResult = await getUser(userId)
277277+278278+ return matchResult(
279279+ async (user) => {
280280+ const ordersResult = await getOrders(user.id)
281281+ return matchResult(
282282+ orders => ok({ user, orders }),
283283+ error => err(error)
284284+ )(ordersResult)
285285+ },
286286+ async (error) => err(error)
287287+ )(userResult)
288288+}
289289+```
290290+291291+Now callers of `getUserWithOrders` know exactly what can fail:
292292+293293+```typescript
294294+const result = await getUserWithOrders("123")
295295+296296+matchResult(
297297+ ({ user, orders }) => {
298298+ console.log(`${user.name} has ${orders.length} orders`)
299299+ },
300300+ error => {
301301+ // TypeScript knows error is ApiError
302302+ switch (error._tag) {
303303+ case "NetworkError":
304304+ console.error("Network issue:", error.message)
305305+ break
306306+ case "NotFound":
307307+ console.error("User not found")
308308+ break
309309+ case "Unauthorized":
310310+ console.error("Please log in")
311311+ break
312312+ case "ValidationError":
313313+ console.error("Invalid fields:", error.fields)
314314+ break
315315+ case "ServerError":
316316+ console.error("Server error:", error.status)
317317+ break
318318+ }
319319+ }
320320+)(result)
321321+```
322322+323323+### Benefits
324324+325325+1. **Complete type information** - The signature tells you everything that can fail
326326+2. **Exhaustive handling** - TypeScript ensures you handle all error cases
327327+3. **Structured errors** - Each error has relevant context, not just a string
328328+4. **Composable** - Chain operations and errors flow automatically
329329+330330+---
331331+332332+## When to Still Throw
333333+334334+Errors as values aren't for everything. Exceptions are still appropriate for:
335335+336336+### Programmer Errors
337337+338338+Bugs that should never happen in correct code:
339339+340340+```typescript
341341+const assertNonEmpty = <T>(arr: T[]): T[] => {
342342+ if (arr.length === 0) {
343343+ throw new Error("Assertion failed: array is empty")
344344+ }
345345+ return arr
346346+}
347347+```
348348+349349+These indicate bugs, not expected failures. Crashing loudly is the right response.
350350+351351+### Truly Exceptional Conditions
352352+353353+System-level failures that can't be meaningfully handled:
354354+355355+- Out of memory
356356+- Stack overflow
357357+- File system corruption
358358+359359+### Quick Scripts
360360+361361+For throwaway scripts where error handling doesn't matter:
362362+363363+```typescript
364364+// Quick script - exceptions are fine
365365+const data = JSON.parse(fs.readFileSync("config.json", "utf-8"))
366366+```
367367+368368+### The Boundary
369369+370370+Use `tryCatch` to convert exceptions to Results at the boundary:
371371+372372+```typescript
373373+import { tryCatch } from "purus-ts"
374374+375375+const parseJson = (input: string): Result<unknown, unknown> =>
376376+ tryCatch(() => JSON.parse(input))
377377+```
378378+379379+This lets you use exception-based libraries while keeping your code Result-based.
380380+381381+---
382382+383383+## Key Takeaways
384384+385385+1. **Exceptions break types** - `catch (e: unknown)` loses all error information
386386+2. **Result makes errors visible** - The return type shows what can fail
387387+3. **Composition handles errors automatically** - Use `chainResult`, errors flow through
388388+4. **Pattern matching is exhaustive** - Add a new error, compiler tells you where to handle it
389389+5. **Use exceptions for bugs** - Programmer errors should crash, not return Result
390390+391391+The shift from exceptions to errors as values is the foundation of reliable TypeScript. Once you see errors in the type system, you can't unsee the blind spots that exceptions create.
392392+393393+---
394394+395395+## See Also
396396+397397+- [Tutorial Chapter 1: Why Functional TypeScript?](../tutorial/01-why-functional-typescript.md) - Introduction with examples
398398+- [Tutorial Chapter 3: Typed Errors with Result](../tutorial/03-typed-errors-with-result.md) - Hands-on practice
399399+- [Branded Types In Depth](./branded-types.md) - Another compile-time safety technique