Effect helps you deliver production-grade TypeScript applications
An Effect lets you model concurrent programs with type-level error tracking and type-safe dependency injection
Provides a cohesive TypeScript standard library and a growing solution ecosystem
Effect will force or nudge you to do things well (production-grade), which will often require learning
LLMs are very good at Effect, when patterns are clearly established
I have been using Effect for about a year and a half. We chose Effect for a relatively young project because we saw in it the traits that allow writing production-grade, maintainable software.
Since then, we have also used Effect it in green-field project, and would like to build most new services with it.
This is the first in a series where I share Effect in the way I think would make it “click”. It’s not a reference, it’s not a course. It’s an overview that should give you a good idea of its capabilities and what’s available, so that you can use the Effect docs and navigate its API or Effect codebases more effectively (🥁).
This is the first in a series to introduce and learn Effect—sharing what I have learnt.
Note
I use em dashes. I have used em dashes for years. I am not an LLM.
Quick Intro
To me, Effect is primarily a library to write production-grade TypeScript. It aims to cover the needs of a serious production project, facilitating best-practices, covering common needs, filling the gap of an otherwise incoherent ecosystem, not just in terms of code, but in terms of productionazing (e.g. observability). In an Effect engineers can spend their valuable time considering important decisions, including many that often end up forgotten or relegated to tech debt.
On paper that sounds great, but when I show an Effect program, the first reaction is often that the syntax is strange, and the API surface large. And that is true. Initially, Effect can feel foreign to engineers otherwise accustomed to TypeScript. The learning curve is real, but it is a curve that will allow you to write Effect from day 1, and increasingly take advantage of everything else it provides as as you learn.
However, Effect immediately feels more approachable from first principles. And lot of the perceived friction from Effect is in how it forces you to consider important aspects of your application that you may have otherwise ignored. For example, understanding the non-happy path of your programs.
I like to compare Effect to TypeScript:
A whole new syntax that feels like a whole new language—yet it is not.
When you start, it can feel tedious, even getting in the way of getting your job done, but the trade-off is clear if you realize the extra work is just covering your back, and the significant positive impact it can have on the maintainability and stability of a system in the long run.
Like TypeScript, you can start adopting it gradually.
Like TypeScript, it will make your code easier to maintain and refactor, especially in teams.
TypeScript has become the standard way to write JavaScript, and I hope Effect will become the standard way to write production TypeScript.
Effects
At the core of the Effect library is the Effect type. If you understand what an Effect is, you can make sense of a lot of the library.
You can think of an Effect as a Promise with super powers. Like a Promise, an Effect can model an asynchronous computation, but it makes complex concurrency scenarios simple to describe. Like a Promise, it will end up with a result, but it tracks the possible errors at the type level. And additionally, it tracks dependencies (aka requirements) for the computation at the type level, which must be provided.
At the type level, Effect takes 3 type arguments Effect, although normally you don’t need to provide them because they are inferred.
1
Effect<Result, Errors, Requirements>
Simple enough, right? Just looking at the types, you can imagine how to represent:
An Effect that always succeeds with a number: Effect<number, never, never>
An Effect that may fail with ErrorA and ErrorB: Effect<Result, never, never>
An Effect that always fails with ErrorA: Effect<never, ErrorA>
Tip
I will cover requirements in more detail in a future post, but for now I want to mention that, by tracking them, Effect enables type-safe dependency injection. These dependencies can be organized and layers, and provided to Effects. This unlocks better code organization and more sane test practices.
Let’s compare how we operate with Promise vs Effect:
Promise
Effect
await somePromise
yield* someEffect
new Promise(function() {})
Effect.gen(function* () {})
async function() {}
Effect.fn(function* () {})
Promise.resolve('Hi')
Effect.succeed('Hi')
Promise.reject('Some error')
Effect.fail('Some error')
So yes, it’s different. Yet similar.
However, Effects describe a computation, so the following will not do anything until executed.
Provides a way to write effectful code using generator functions, simplifying
control flow and error handling.
When to Use
Effect.gen allows you to write code that looks and behaves like synchronous
code, but it can handle asynchronous tasks, errors, and complex control flow
(like loops and conditions). It helps make asynchronous code more readable
and easier to manage.
The generator functions work similarly to async/await but with more
explicit control over the execution of effects. You can yield* values from
effects and return the final result at the end.
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
A Console class with methods such as console.log(), console.error() andconsole.warn() that can be used to write to any Node.js stream.
A global console instance configured to write to process.stdout and process.stderr. The global console can be used without callingrequire('console').
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
Prints to stdout with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3) (the arguments are all passed to util.format()).
constcount=5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format() for more information.
@since ― v0.1.100
log('Hello Effect!')
3
})
This is unlike Promises. The following will log to the console, even if you don’t await or do anything with it.
1
const
constmyPromise:Promise<unknown>
myPromise=new
var Promise:PromiseConstructor
new <unknown>(executor: (resolve: (value:unknown) =>void, reject: (reason?:any) =>void) =>void) =>Promise<unknown>
Creates a new Promise.
@param ― executor A callback used to initialize the promise. This callback is passed two arguments:
a resolve callback used to resolve the promise with a value or the result of another promise,
and a reject callback used to reject the promise with a provided reason or error.
Promise(() =>
var console:Console
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
A Console class with methods such as console.log(), console.error() andconsole.warn() that can be used to write to any Node.js stream.
A global console instance configured to write to process.stdout and process.stderr. The global console can be used without callingrequire('console').
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
Prints to stdout with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3) (the arguments are all passed to util.format()).
constcount=5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format() for more information.
@since ― v0.1.100
log('Hello Promise!'))
Effects are executed by a runtime. I’ll cover runtimes in a future post, but for now it’s sufficient to know that you can execute an Effect in a default runtime with Effect.runPromise(effect), which will return a Promise. It’s like turning an Effect into a Promise, really. And you can do the opposite too, with Effect.promise(() => promise).
This shows a Promise calling an Effect that calls a Promise:
1
import {
import Effect
@since ― 2.0.0
@since ― 2.0.0
@since ― 2.0.0
Effect } from'effect';
2
3
const
constbirthDate:Promise<string>
birthDate=
var Promise:PromiseConstructor
Represents the completion of an asynchronous operation
Provides a way to write effectful code using generator functions, simplifying
control flow and error handling.
When to Use
Effect.gen allows you to write code that looks and behaves like synchronous
code, but it can handle asynchronous tasks, errors, and complex control flow
(like loops and conditions). It helps make asynchronous code more readable
and easier to manage.
The generator functions work similarly to async/await but with more
explicit control over the execution of effects. You can yield* values from
effects and return the final result at the end.
Creates an Effect that represents an asynchronous computation guaranteed to
succeed.
Details
The provided function (thunk) returns a Promise that should never reject; if it does, the error
will be treated as a "defect".
This defect is not a standard error but indicates a flaw in the logic that
was expected to be error-free. You can think of it similar to an unexpected
crash in the program, which can be further managed or logged using tools like
catchAllDefect
.
Interruptions
An optional AbortSignal can be provided to allow for interruption of the
wrapped Promise API.
When to Use
Use this function when you are sure the operation will not reject.
Executes an effect and returns the result as a Promise.
Details
This function runs an effect and converts its result into a Promise. If the
effect succeeds, the Promise will resolve with the successful result. If
the effect fails, the Promise will reject with an error, which includes the
failure details of the effect.
The optional options parameter allows you to pass an AbortSignal for
cancellation, enabling more fine-grained control over asynchronous tasks.
When to Use
Use this function when you need to execute an effect and work with its result
in a promise-based system, such as when integrating with third-party
libraries that expect Promise results.
Example (Running a Successful Effect as a Promise)
@see ― runPromiseExit for a version that returns an Exit type instead
of rejecting.
@since ― 2.0.0
runPromise(
constperson:Effect.Effect<{
name:string;
birthDate:string;
}, never, never>
person);
13
}
With those two methods, you have full interop, and the ability to integrate Effect anywhere in an existing program, without having to refactor it all. Eventually you might be tempted to adopt Effect everywhere, but you don’t have to.
The Effect library can acquire and release resources for an Effect, schedule Effects, execute them sequentially or concurrently, etc. This means we can model complex behaviors such as exponential backoff simply. https://effect.kitlangton.com/ does a really good job at explaining concurrency tools in Effect with visualizations.
The Effect ecosystem
Effect includes what could be described as the ideal TypeScript standard library that never was. With it, streams, dates, decimals or arrays gain a predictable API.
@effect/platform is a package that provides means to interact with the runtime (think node, bun or the browser), create HTTP servers, create REST APIs declaratively, and more.
Effect also has official packages to work with databases (@effect/sql), automatic OTEL observability (@effect/opentelemetry), serve and consume RPC (@effect/rpc), create clusters of programs that can talk to each other (@effect/cluster), define durable workflows (@effect/workflow), create CLI commands (@effect/cli), work with AI (@effect/ai), and more.
I plan to cover all of the above in more detail in future posts.
I have found that Effect projects tend to have less dependencies, and that we spend less time managing our dependencies, because there is no glue work nor version conflicts. This is a major pain point of the JavaScript ecosystem that Effect relieves.
On the frontend side, you may want to check out effect-atom. Effect is more useful in the backend currently, but I expect that to change over time.
Effect compounds
If you compare a specific use-case of Effect against the code to do the same without Effect, you might not be convinced. The value is more obvious when you think about maintainability, production needs, and the life-cycle of your program. The example above comparing Effect and Promise might help understand the Effect type, but it doesn’t necessarily sell Effect.
There was a recent interaction on X looking at caching an operation. Doing it without Effect is simple enough. But the Effect approach is:
declarative: you say “this Effect should be cached”, vs your inner logic saying “let me cache this”, “let me read from cache now”
reusable: the same declarative approach works for any Effect anywhere in your app
composable: similarly add error handling, retry policies, batching on top of it—all still declaratively