| 1 | # Fiber Keep-Alive: Automatic Process Lifetime Management |
| 2 | |
| 3 | In v3, the core `effect` runtime did **not** keep the Node.js process alive while |
| 4 | fibers were suspended on certain asynchronous operations. If a fiber was waiting on |
| 5 | something like `Deferred.await` and there was no other work scheduled on the |
| 6 | event loop, the process would exit immediately — the fiber's suspension did not |
| 7 | register as pending work from Node.js's perspective. |
| 8 | |
| 9 | The only way to prevent this was to use `runMain` from `@effect/platform-node` |
| 10 | (or `@effect/platform-bun`), which installed a long-lived `setInterval` timer |
| 11 | to hold the process open until the root fiber completed. |
| 12 | |
| 13 | In v4, **the keep-alive mechanism is built into the core runtime**. |
| 14 | |
| 15 | ## The Problem in v3 |
| 16 | |
| 17 | Consider the following program: |
| 18 | |
| 19 | ```ts |
| 20 | import { Deferred, Effect } from "effect" |
| 21 | |
| 22 | const program = Effect.gen(function*() { |
| 23 | const deferred = yield* Deferred.make<string>() |
| 24 | |
| 25 | yield* Deferred.await(deferred) |
| 26 | }) |
| 27 | |
| 28 | Effect.runPromise(program) |
| 29 | ``` |
| 30 | |
| 31 | In v3, when the main fiber reached `yield* Deferred.await(deferred)`, it suspended |
| 32 | while waiting for the worker fiber to complete the deferred. However, from the |
| 33 | JavaScript runtime's perspective, the event loop had no more work to do. Thus, |
| 34 | the process would exit. |
| 35 | |
| 36 | The workaround was to use `runMain` from the platform package, which installs |
| 37 | a timer that holds the process open until the root fiber completes: |
| 38 | |
| 39 | ```ts |
| 40 | import { NodeRuntime } from "@effect/platform-node" |
| 41 | |
| 42 | NodeRuntime.runMain(program) |
| 43 | ``` |
| 44 | |
| 45 | ## What Changed in v4 |
| 46 | |
| 47 | In v4, the Effect fiber runtime automatically manages a reference-counted |
| 48 | keep-alive timer. |
| 49 | |
| 50 | This means the following program works in v4 **without** `runMain`: |
| 51 | |
| 52 | ```ts |
| 53 | import { Deferred, Effect, Fiber } from "effect" |
| 54 | |
| 55 | const program = Effect.gen(function*() { |
| 56 | const deferred = yield* Deferred.make<string>() |
| 57 | |
| 58 | // The process stays alive while waiting — no runMain needed |
| 59 | yield* Deferred.await(deferred) |
| 60 | }) |
| 61 | |
| 62 | Effect.runPromise(program) |
| 63 | ``` |
| 64 | |
| 65 | ## `runMain` Is Still Recommended |
| 66 | |
| 67 | Even though the core runtime now handles keep-alive, `runMain` from the platform |
| 68 | packages is still the recommended way to run Effect programs. It provides: |
| 69 | |
| 70 | - **Signal handling** — listens for `SIGINT` / `SIGTERM` and interrupts the |
| 71 | root fiber gracefully. |
| 72 | - **Exit code management** — calls `process.exit(code)` when the program fails |
| 73 | or receives a signal. |
| 74 | - **Error reporting** — reports unhandled errors to the console. |