TL;DR (Quick Summary)#
- Pure Values: Use
Effect.succeed(value)andEffect.fail(error)for hardcoded, static values. - Synchronous Logic: Use
Effect.sync(() => doMath())for safe synchronous execution. - Asynchronous Logic (Promises): Use
Effect.tryPromise()to wrap native Promises and safely catch their rejections into typed Errors. - Legacy Callbacks: Use
Effect.async(resume)to bridge older Node.js callback APIs (likefs.readFile) into modern Effects. - Execution: Effects do nothing until you hand them to the Runtime using
Effect.runPromise(program)orEffect.runSync(program).
1. Introduction: Bridging the Gap#
When migrating an application to Effect-TS, you will inevitably interact with third-party libraries (like Prisma, Axios, or the AWS SDK). These libraries do not know what an Effect is. They return native Promises, throw random exceptions, or use Node.js (err, data) callbacks.
To maintain architectural purity, we must “wrap” these external boundaries into the Effect ecosystem immediately. This is known as the Anti-Corruption Layer pattern.
2. Wrapping Pure and Synchronous Values#
Static Values (succeed and fail)#
If you have a value that is already computed and you simply want to lift it into the Effect pipeline, use Effect.succeed or Effect.fail.
import { Effect } from "effect";
// 🟢 Effect<number, never, never>
const staticSuccess = Effect.succeed(42);
// 🔴 Effect<never, string, never>
const staticFailure = Effect.fail("Something went wrong");Lazy Synchronous Execution (sync)#
If your synchronous code involves computation or side effects (like generating a random number or logging to the console), you must use Effect.sync.
// 🔴 Bad: The random number is generated immediately (Eager)
const badRandom = Effect.succeed(Math.random());
// 🟢 Good: The computation is deferred until runtime (Lazy)
const goodRandom = Effect.sync(() => Math.random());3. Wrapping Asynchronous Promises#
The vast majority of your I/O operations will be Promise-based.
The Unsafe Way (Effect.promise)#
If you are 100% mathematically certain that a Promise will never reject, you can use Effect.promise().
const delay = Effect.promise(() =>
new Promise<void>(resolve => setTimeout(resolve, 1000))
);The Safe Way (Effect.tryPromise)#
Almost all Promises (especially network requests or database queries) can reject. If you use Effect.promise() and it rejects, the Effect runtime treats it as an Unexpected Defect and crashes the fiber.
To safely catch the rejection and turn it into a Typed Error (E), you must use Effect.tryPromise().
const fetchUser = (id: string) => Effect.tryPromise({
// 1. The Promise to execute
try: () => fetch(`/api/users/${id}`).then(res => res.json()),
// 2. The mapping function to catch the unknown rejection
catch: (unknownError) => new Error(`Network failure: ${String(unknownError)}`)
});
// Signature: Effect<any, Error, never>
By providing the catch block, the compiler knows exactly what type of error this pipeline produces, ensuring you don’t forget to handle it later!
4. Wrapping Legacy Callbacks#
If you are working with legacy Node.js APIs or event listeners, you will encounter the (err, data) => void callback pattern. You can wrap these using Effect.async.
Effect.async gives you a resume function. You call resume(Effect.succeed(...)) when the callback succeeds, or resume(Effect.fail(...)) when it fails.
import * as fs from "node:fs";
import { Effect } from "effect";
const readFileEffect = (path: string) => Effect.async<string, Error>(resume => {
fs.readFile(path, "utf8", (err, data) => {
if (err) {
resume(Effect.fail(err));
} else {
resume(Effect.succeed(data));
}
});
});
// Signature: Effect<string, Error, never>
5. Effect Creators Comparison#
| Source Data | Use Constructor | When to use |
|---|---|---|
| Static Value | Effect.succeed(val) | Hardcoded values, pure logic results. |
| Sync Execution | Effect.sync(() => doX()) | console.log, Math.random(), sync operations. |
| Safe Promise | Effect.promise(() => doX()) | Promises that literally cannot reject. |
| Unsafe Promise | Effect.tryPromise({ try, catch }) | Database calls, HTTP requests, file I/O. |
| Callbacks | Effect.async(resume => ...) | Event listeners, legacy Node.js APIs. |
6. Executing the Pipeline#
Once you have constructed your massive, lazy pipeline of Effects, it is time to execute it at the edge of your application.
Synchronous Execution#
If your pipeline contains zero asynchronous boundaries (no Promises, no async callbacks), you can run it synchronously.
const syncProgram = Effect.sync(() => 10 * 10);
// Executes instantly and blocks the thread
const result = Effect.runSync(syncProgram);
console.log(result); // 100
Asynchronous Execution#
If your pipeline contains even a single asynchronous boundary, you must run it returning a Promise.
const asyncProgram = Effect.tryPromise({ ... });
// Executes and returns a standard Promise
Effect.runPromise(asyncProgram).then(data => {
console.log("Success:", data);
}).catch(err => {
console.error("Pipeline Failed:", err);
});(Note: There is also Effect.runFork() for background daemon execution, which we will cover in the Concurrency episode.)
7. Troubleshooting & Common Errors#
Error 1: Fiber cannot be executed synchronously#
Error: Fiber cannot be executed synchronously.The Cause: You built a pipeline using Effect.promise or Effect.async, but you attempted to execute it using Effect.runSync().
The Fix: You cannot block a Node.js/V8 thread to wait for a Promise natively. You must use Effect.runPromise().
Error 2: Uncaught Defects from Effect.promise#
The Mistake: You used Effect.promise() to wrap an Axios call, thinking the network would always succeed. When the network dropped, the Promise rejected. Because Effect.promise assumes no errors, the rejection was treated as a fatal defect, bypassing your catchAll blocks.
The Fix: Always, always use Effect.tryPromise for network and I/O boundaries.
Summary & Next Steps#
In this episode:
- We wrapped static values using
Effect.succeedandEffect.fail. - We deferred synchronous execution using
Effect.sync. - We safely caught Promise rejections and typed them using
Effect.tryPromise. - We bridged legacy callback hell into functional purity using
Effect.async. - We executed our lazy pipelines at the edge of our application.
Now that we know how to push our Typed Errors (E) into the Effect pipeline, how do we handle them?
In Episode 64: Error Handling in Effect, we will explore how Effect-TS completely eliminates try/catch spaghetti code through elegant Pattern Matching!

