···11+# Chapter 2: Your First Effect
22+33+In the previous chapter, we saw how `Result<T, E>` makes errors visible in the type system. But Result is synchronous - it holds a value that already exists.
44+55+What about async operations? Fetching data, reading files, calling APIs - these are where most errors happen, and where we need the most help.
66+77+This chapter introduces `Eff<A, E, R>` - purus's Effect type for composable async operations.
88+99+---
1010+1111+## What is an Effect?
1212+1313+An Effect is a **description of work to be done**, not the work itself.
1414+1515+```typescript
1616+import { succeed, runPromise } from "purus-ts"
1717+1818+// This doesn't print anything yet
1919+const greeting = succeed("Hello, World!")
2020+2121+// Now it runs
2222+const result = await runPromise(greeting)
2323+console.log(result) // "Hello, World!"
2424+```
2525+2626+This might seem like extra steps for no benefit. But laziness is the key to composition.
2727+2828+### Why Laziness Matters
2929+3030+Consider this Promise-based code:
3131+3232+```typescript
3333+const fetchUser = fetch("/user/1") // Request starts IMMEDIATELY
3434+const fetchOrders = fetch("/orders") // This too, even if we don't need it
3535+3636+// What if we wanted to run them sequentially?
3737+// Too late - they're already running in parallel
3838+```
3939+4040+With Effects:
4141+4242+```typescript
4343+const fetchUser = fromPromise(() => fetch("/user/1")) // Nothing happens
4444+const fetchOrders = fromPromise(() => fetch("/orders")) // Still nothing
4545+4646+// Now we choose: sequential or parallel
4747+const sequential = pipe(fetchUser, flatMap(() => fetchOrders))
4848+const parallel = all([fetchUser, fetchOrders])
4949+```
5050+5151+Effects let you describe what should happen, then decide how and when to run it.
5252+5353+---
5454+5555+## The Eff Type
5656+5757+An Effect has three type parameters:
5858+5959+```typescript
6060+Eff<A, E, R>
6161+// ^ ^ ^
6262+// | | └── R: Requirements (dependencies the effect needs)
6363+// | └───── E: Error type (what can go wrong)
6464+// └──────── A: Success type (what you get if it works)
6565+```
6666+6767+For now, we'll focus on A and E. We'll cover R (requirements) in Chapter 9.
6868+6969+---
7070+7171+## Creating Effects
7272+7373+### succeed() - A value that's already computed
7474+7575+```typescript
7676+import { succeed } from "purus-ts"
7777+7878+const five = succeed(5)
7979+// Type: Eff<number, never, unknown>
8080+// never error type means it can't fail
8181+```
8282+8383+### fail() - An error
8484+8585+```typescript
8686+import { fail } from "purus-ts"
8787+8888+type MyError = { _tag: "MyError"; message: string }
8989+9090+const oops = fail<MyError>({ _tag: "MyError", message: "Something went wrong" })
9191+// Type: Eff<never, MyError, unknown>
9292+// never success type means it always fails
9393+```
9494+9595+### sync() - Synchronous computation
9696+9797+```typescript
9898+import { sync } from "purus-ts"
9999+100100+const now = sync(() => new Date())
101101+// Type: Eff<Date, never, unknown>
102102+// The function runs each time the effect is executed
103103+```
104104+105105+Use `sync()` when you need to delay a synchronous computation:
106106+107107+```typescript
108108+// Without sync - computes immediately
109109+const immediate = succeed(Math.random()) // Same value every time
110110+111111+// With sync - computes when run
112112+const delayed = sync(() => Math.random()) // New value each time
113113+```
114114+115115+### attempt() - Catch thrown exceptions
116116+117117+```typescript
118118+import { attempt } from "purus-ts"
119119+120120+const parsed = attempt(() => JSON.parse(userInput))
121121+// Type: Eff<unknown, unknown, unknown>
122122+// If JSON.parse throws, it becomes an error value
123123+```
124124+125125+The error type is `unknown` because we don't know what might be thrown. You can refine this:
126126+127127+```typescript
128128+import { pipe, catchAll, fail } from "purus-ts"
129129+130130+type ParseError = { _tag: "ParseError"; input: string }
131131+132132+const safeParse = (input: string) => pipe(
133133+ attempt(() => JSON.parse(input)),
134134+ catchAll(() => fail<ParseError>({ _tag: "ParseError", input }))
135135+)
136136+// Type: Eff<unknown, ParseError, unknown>
137137+```
138138+139139+### fromPromise() - Bridge from Promises
140140+141141+```typescript
142142+import { fromPromise } from "purus-ts"
143143+144144+const fetchData = fromPromise(() => fetch("/api/data").then(r => r.json()))
145145+// Type: Eff<unknown, unknown, unknown>
146146+```
147147+148148+The function returns a Promise. It's wrapped in a thunk `() =>` so it doesn't start immediately.
149149+150150+### async() - Full control over async behavior
151151+152152+For advanced cases where you need cleanup or cancellation:
153153+154154+```typescript
155155+import { async, Exit } from "purus-ts"
156156+157157+const delay = (ms: number) => async<void, never>(resume => {
158158+ const id = setTimeout(() => resume(Exit.succeed(undefined)), ms)
159159+ return () => clearTimeout(id) // Cleanup function
160160+})
161161+```
162162+163163+We'll explore `async()` more in Chapter 8 on concurrency.
164164+165165+---
166166+167167+## Running Effects
168168+169169+Effects are just descriptions until you run them.
170170+171171+### runPromise() - Run and get the value
172172+173173+```typescript
174174+import { succeed, runPromise } from "purus-ts"
175175+176176+const effect = succeed(42)
177177+const value = await runPromise(effect)
178178+console.log(value) // 42
179179+```
180180+181181+If the effect fails, `runPromise` throws:
182182+183183+```typescript
184184+import { fail, runPromise } from "purus-ts"
185185+186186+const effect = fail({ _tag: "Oops" })
187187+await runPromise(effect) // Throws!
188188+```
189189+190190+### runPromiseExit() - Run and get success or failure
191191+192192+For handling both cases:
193193+194194+```typescript
195195+import { succeed, fail, runPromiseExit } from "purus-ts"
196196+197197+const exit1 = await runPromiseExit(succeed(42))
198198+// { _tag: "Success", value: 42 }
199199+200200+const exit2 = await runPromiseExit(fail({ _tag: "Oops" }))
201201+// { _tag: "Failure", error: { _tag: "Oops" } }
202202+```
203203+204204+This is useful for testing and when you need to handle errors at the top level.
205205+206206+---
207207+208208+## Transforming Effects
209209+210210+### mapEff() - Transform the success value
211211+212212+```typescript
213213+import { succeed, mapEff, pipe, runPromise } from "purus-ts"
214214+215215+const doubled = pipe(
216216+ succeed(21),
217217+ mapEff(n => n * 2)
218218+)
219219+220220+await runPromise(doubled) // 42
221221+```
222222+223223+If the effect fails, `mapEff` does nothing - the error passes through.
224224+225225+### flatMap() - Chain effects together
226226+227227+This is the core composition operator. Use it when one effect depends on another:
228228+229229+```typescript
230230+import { succeed, flatMap, pipe, runPromise } from "purus-ts"
231231+232232+const program = pipe(
233233+ succeed(10),
234234+ flatMap(a => succeed(a + 5)),
235235+ flatMap(b => succeed(b * 2))
236236+)
237237+238238+await runPromise(program) // 30
239239+```
240240+241241+`flatMap` is like `then()` for Promises, but:
242242+- It's lazy (nothing runs until `runPromise`)
243243+- It preserves error types
244244+- It composes with other combinators
245245+246246+### catchAll() - Handle errors
247247+248248+```typescript
249249+import { fail, catchAll, succeed, pipe, runPromise } from "purus-ts"
250250+251251+const recovered = pipe(
252252+ fail({ _tag: "NetworkError", message: "timeout" }),
253253+ catchAll(error => {
254254+ console.log(`Caught: ${error.message}`)
255255+ return succeed("fallback value")
256256+ })
257257+)
258258+259259+await runPromise(recovered) // "fallback value"
260260+```
261261+262262+### foldEff() - Handle both success and failure
263263+264264+```typescript
265265+import { succeed, foldEff, pipe, runPromise } from "purus-ts"
266266+267267+const result = pipe(
268268+ succeed(42),
269269+ foldEff(
270270+ error => succeed(`Error: ${error}`),
271271+ value => succeed(`Success: ${value}`)
272272+ )
273273+)
274274+275275+await runPromise(result) // "Success: 42"
276276+```
277277+278278+---
279279+280280+## The pipe() Pattern
281281+282282+`pipe()` is how you compose operations in purus:
283283+284284+```typescript
285285+import { pipe, succeed, mapEff, flatMap } from "purus-ts"
286286+287287+const result = pipe(
288288+ succeed(10), // Start with 10
289289+ mapEff(n => n * 2), // Transform to 20
290290+ flatMap(n => succeed(n + 5)), // Chain to 25
291291+ mapEff(String) // Convert to "25"
292292+)
293293+```
294294+295295+Read it top-to-bottom: start with a value, apply each transformation in order.
296296+297297+Without `pipe()`, you'd write:
298298+299299+```typescript
300300+mapEff(String)(flatMap(n => succeed(n + 5))(mapEff(n => n * 2)(succeed(10))))
301301+```
302302+303303+Which is inside-out and hard to read.
304304+305305+---
306306+307307+## Exercise: Build a Validator
308308+309309+Let's practice by building a simple input validator.
310310+311311+### The Goal
312312+313313+Create a function that validates user input:
314314+- Name must be non-empty
315315+- Age must be a positive number
316316+- Email must contain @
317317+318318+### Step 1: Define the types
319319+320320+```typescript
321321+type ValidationError =
322322+ | { _tag: "EmptyName" }
323323+ | { _tag: "InvalidAge"; value: number }
324324+ | { _tag: "InvalidEmail"; value: string }
325325+326326+type UserInput = {
327327+ name: string
328328+ age: number
329329+ email: string
330330+}
331331+332332+type ValidUser = {
333333+ name: string
334334+ age: number
335335+ email: string
336336+}
337337+```
338338+339339+### Step 2: Create validators
340340+341341+```typescript
342342+import { type Eff, succeed, fail } from "purus-ts"
343343+344344+const validateName = (name: string): Eff<string, ValidationError, unknown> =>
345345+ name.trim().length > 0
346346+ ? succeed(name.trim())
347347+ : fail({ _tag: "EmptyName" })
348348+349349+const validateAge = (age: number): Eff<number, ValidationError, unknown> =>
350350+ age > 0
351351+ ? succeed(age)
352352+ : fail({ _tag: "InvalidAge", value: age })
353353+354354+const validateEmail = (email: string): Eff<string, ValidationError, unknown> =>
355355+ email.includes("@")
356356+ ? succeed(email)
357357+ : fail({ _tag: "InvalidEmail", value: email })
358358+```
359359+360360+### Step 3: Compose them
361361+362362+```typescript
363363+import { pipe, flatMap, mapEff } from "purus-ts"
364364+365365+const validateUser = (input: UserInput): Eff<ValidUser, ValidationError, unknown> =>
366366+ pipe(
367367+ validateName(input.name),
368368+ flatMap(name =>
369369+ pipe(
370370+ validateAge(input.age),
371371+ flatMap(age =>
372372+ pipe(
373373+ validateEmail(input.email),
374374+ mapEff(email => ({ name, age, email }))
375375+ )
376376+ )
377377+ )
378378+ )
379379+ )
380380+```
381381+382382+### Step 4: Use it
383383+384384+```typescript
385385+import { runPromiseExit } from "purus-ts"
386386+387387+const goodInput = { name: "Alice", age: 30, email: "alice@example.com" }
388388+const badInput = { name: "", age: -5, email: "no-at-sign" }
389389+390390+const result1 = await runPromiseExit(validateUser(goodInput))
391391+// { _tag: "Success", value: { name: "Alice", age: 30, email: "alice@example.com" } }
392392+393393+const result2 = await runPromiseExit(validateUser(badInput))
394394+// { _tag: "Failure", error: { _tag: "EmptyName" } }
395395+// Note: Fails on first error (short-circuit behavior)
396396+```
397397+398398+---
399399+400400+## Key Takeaways
401401+402402+1. **Effects are descriptions** - They don't run until you call `runPromise`
403403+2. **Laziness enables composition** - You decide when and how to run
404404+3. **Eff<A, E, R>** - Success type, Error type, Requirements
405405+4. **flatMap chains effects** - Like Promise.then, but lazy and typed
406406+5. **pipe() reads top-to-bottom** - Cleaner than nested function calls
407407+408408+---
409409+410410+## What's Next
411411+412412+In the next chapter, we'll dive deeper into `Result<T, E>` for synchronous error handling - the building block that makes typed errors possible.
413413+414414+[Continue to Chapter 3: Typed Errors with Result →](./03-typed-errors-with-result.md)