TL;DR (Quick Summary)#
- What is a Fiber?: A Fiber is a “Green Thread”. It is an incredibly lightweight, simulated thread managed by the Effect Runtime, not the Operating System. You can spawn millions of them.
- The Problem with Promises: Once a native
Promisestarts, it cannot be stopped. If a user cancels an HTTP request, the backend Promise keeps running, wasting database connections. - Interruption: Fibers are fully interruptible. If a parent Fiber fails, all child Fibers are safely cancelled automatically.
- High-Level Concurrency: Use
Effect.all(effects, { concurrency: 10 })to run an array of effects concurrently with a strict worker limit, replacingPromise.all.
1. Introduction: The Native Promise Problem#
When you need to fetch data for 50 users simultaneously in standard TypeScript, you use Promise.all.
// 🔴 Native Promise Concurrency
async function fetchAllUsers(userIds: string[]) {
const promises = userIds.map(id => fetchUser(id)); // 50 HTTP requests start IMMEDIATELY
try {
return await Promise.all(promises);
} catch (error) {
console.error("One request failed!");
}
}This code has two massive flaws:
- No Throttling: It blasts 50 HTTP requests at the exact same millisecond. If the array had 10,000 IDs, it would crash your Node.js server with an Out of Memory (OOM) error or get you rate-limited by the API.
- No Cancellation: If the very first request fails,
Promise.allthrows the error immediately. However, the other 49 HTTP requests keep running in the background! They waste bandwidth, CPU, and database connections because Promises cannot be cancelled.
2. What are Fibers?#
Languages like Go have “Goroutines”, and Erlang has “Processes”. These are lightweight concurrency primitives managed by the language runtime, not the OS thread scheduler.
Effect-TS brings this exact concept to TypeScript via Fibers.
When you call Effect.runPromise(program), the Effect Runtime spawns a Main Fiber. Every asynchronous operation inside that pipeline executes on that Fiber. Because the Runtime controls the execution loop, it can pause, resume, or completely destroy the Fiber at any millisecond.
3. High-Level Concurrency (Effect.all)#
You rarely need to interact with Fibers directly. The Effect API provides high-level tools built on top of Fibers.
Let’s rewrite our fetchAllUsers function using Effect.all.
import { Effect } from "effect";
const userIds = ["1", "2", "3", /* ... up to 50 */];
// Create an array of LAZY effects
const userEffects = userIds.map(id => fetchUserEffect(id));
// 🟢 Safe, Throttled, Interruptible Concurrency
const program = Effect.all(userEffects, {
concurrency: 5 // Run exactly 5 requests at a time!
});
Effect.runPromise(program).then(console.log);Why Effect.all is superior:#
- Throttling: By passing
{ concurrency: 5 }, the Effect runtime spawns exactly 5 Fibers. As soon as one finishes, it pulls the next Effect from the queue. You will never crash your server. - Safe Interruption: If the 3rd request fails,
Effect.allimmediately sends an Interruption Signal to the other 4 active Fibers. They are safely killed, dropping their HTTP connections and saving your server resources!
4. Low-Level Concurrency (Forking)#
Sometimes you want to intentionally spawn a background task (a daemon) that runs independently of your main pipeline. You do this by “forking” a new Fiber using Effect.fork.
import { Effect, Fiber, Console } from "effect";
const backgroundTask = Effect.pipe(
Effect.sleep("5 seconds"),
Effect.flatMap(() => Console.log("Background task finished!"))
);
const mainProgram = Effect.pipe(
Console.log("Starting main program..."),
// 🟢 Spawn the background task on a NEW Fiber
Effect.flatMap(() => Effect.fork(backgroundTask)),
// 'fiber' is a reference to the running background task
Effect.flatMap(fiber =>
Effect.pipe(
Console.log("Doing other work..."),
Effect.sleep("2 seconds"),
// 🟢 We can manually kill the background task!
Effect.flatMap(() => Fiber.interrupt(fiber)),
Effect.flatMap(() => Console.log("Killed the background task before it finished."))
)
)
);
Effect.runPromise(mainProgram);
// Output:
// Starting main program...
// Doing other work...
// Killed the background task before it finished.
5. Comparison: Promise.all vs Effect.all#
| Feature | Promise.all | Effect.all |
|---|---|---|
| Execution | Eager (Fires instantly) | Lazy (Waits for Runtime) |
| Throttling | None (Requires 3rd party libs like p-limit) | Native ({ concurrency: N }) |
| Failure Behavior | Rejects early, but sibling promises keep running blindly. | Rejects early, and sends kill signals to all sibling Fibers. |
| Type Safety | Errors are any | Errors are mathematically tracked in the E channel. |
6. Troubleshooting & Common Errors#
Error 1: Ignoring the Fiber Reference#
The Mistake: You use Effect.fork(task), but you don’t save the Fiber reference it returns. You let the main program exit.
The Cause: If the Main Fiber exits, all child Fibers it spawned via Effect.fork are automatically interrupted and killed!
The Fix: If you want a Fiber to survive after the main program exits, you must use Effect.forkDaemon(task). Be careful, as daemon fibers can cause memory leaks if never joined or interrupted.
Error 2: Blocking the Event Loop#
The Mistake: You write a massive, computationally expensive while(true) loop inside Effect.sync.
The Cause: Fibers are simulated on top of Node.js. If you write synchronous code that blocks the V8 thread for 10 seconds, the Effect Runtime cannot pause it or switch to other Fibers!
The Fix: If you have massive mathematical computations, you must periodically use Effect.yieldNow() to explicitly hand control back to the Effect Runtime, allowing it to schedule other Fibers.
Summary & Next Steps#
In this episode:
- We identified the resource-wasting flaws of native
Promise.all. - We defined Fibers as lightweight, interruptible “Green Threads”.
- We used
Effect.allto execute concurrent tasks with strict limits and safe interruption. - We used
Effect.forkto spawn background tasks andFiber.interruptto kill them manually.
Our pipelines are now blazingly fast and completely safe. But what happens when the data we fetch from the network is completely malformed? How do we validate JSON payloads before they enter our pure functional domain?
In Episode 68: Effect Schema Validation, we will replace Zod and Joi with the ultimate functional validation library!

