Effect?

I've been working with TypeScript for years, and I've always been seeking better ways to handle side effects, errors, and dependencies. When I discovered Effect, it changed my approach to functional programming completely.
Effect isn't just another TypeScript library. As Ethan Niser explains in his insightful article:
Effect is a language.
Specifically, Effect is an attempt to answer a question that many people have asked, and a few have answered: what would it look like if we had a language for describing effectful computations?
This realization hit me when I was reimagining a GitHub followers tracking tool. Effect isn't merely a collection of utility functions—it's a comprehensive approach to handling effectful computations within TypeScript.
What makes Effect special is how it extends existing tools:
Promise
by making laziness, error handling, retries, interruption, and observability first-class citizensLet me show you the contrast between traditional Promise approaches and Effect:
flowchart LR
subgraph "Traditional Promise Approach"
A[Call API] --> B{Success?}
B -->|Yes| C[Process Data]
B -->|No| D[Catch Error]
D --> E[Log or Rethrow]
end
subgraph "Effect Approach"
F[Define Effect] --> G[Compose with other Effects]
G --> H[Handle all errors]
H --> I[Inject dependencies]
I --> J[Run the Effect]
end
style F fill:#d4f1f9
style G fill:#d4f1f9
style H fill:#d4f1f9
style I fill:#d4f1f9
style J fill:#d4f1f9
With traditional Promises, error handling is bolted on with .catch()
. With Effect, errors are part of the type signature itself - you simply can't forget to handle them.
Before diving into my implementation, I should note that Effect isn't without its tradeoffs:
Despite these considerations, for many applications, the benefits significantly outweigh these costs.
To test these ideas of this new programming language, I took an existing tool—GitHub Followers Watch originally written in Go—and reimplemented it as GitHub Watcher using TypeScript and Effect.
The original tool's purpose was simple: track GitHub followers and following lists to monitor changes over time. But rebuilding it with Effect demonstrated some powerful concepts:
flowchart TD
A[GitHub API] --> B[GitHub API Layer]
B --> C[Effect Program]
C --> D{Command Line Interface}
D --> E[List Followers Command]
D --> F[List Following Command]
E --> G[Output to Console/File]
F --> G
H[Environment Variables] --> B
export class GitHubApiError extends Error {
readonly _tag = "GitHubApiError";
constructor(message: string) {
super(message);
this.name = "GitHubApiError";
}
}
This isn't just error handling—it's precise, typed error handling that lets you know exactly what can fail at compile time.
interface GitHubApi {
readonly getSelfID: Effect.Effect<string, GitHubApiError>;
readonly listAllFollowing: (username: string) => Effect.Effect<string[], GitHubApiError>;
readonly listAllFollowers: (username: string) => Effect.Effect<string[], GitHubApiError>;
}
Look at that interface. The return type tells you everything: what the success value is, what can go wrong, and what dependencies it has—all in one unified type.
Here's how Effect's type signature compares to other approaches:
classDiagram
class Promise {
+then(success)
+catch(error)
-No typed errors
-No dependency tracking
}
class Either {
+map(f)
+flatMap(f)
+Typed errors
-Eager evaluation
-No dependency tracking
}
class Effect {
+map(f)
+flatMap(f)
+provide(dependencies)
+Typed errors
+Dependency tracking
+Resource management
+Lazy evaluation
+Structured concurrency
}
export const printFollowers = Effect.gen(function* (_) {
const api = yield* _(Effect.map(GitHubApiLive, layer => layer.GitHubApi));
const login = yield* _(api.getSelfID);
const followers = yield* _(api.listAllFollowers(login));
return yield* _(Console.log(`${followers.join("\n")}`));
});
This is where Effect shines. The generator syntax makes asynchronous code read like synchronous code, but without losing precise error handling or types.
export const GitHubApiLive = Effect.gen(function* (_) {
const token = yield* _(Effect.try({
try: () => {
const token = process.env.PERSONAL_ACCESS_TOKEN;
if (!token) throw new Error("PERSONAL_ACCESS_TOKEN env var not set");
return token;
},
catch: () => new GitHubApiError("PERSONAL_ACCESS_TOKEN env var not set")
}));
const octokit = new Octokit({ auth: token });
return makeGitHubApi(octokit);
}).pipe(Effect.map(api => ({ GitHubApi: api })));
Dependencies are handled through layers, not some complicated DI container. It's just values all the way down.
Here's a visualization of Effect's dependency injection approach:
flowchart TD
subgraph "Application"
A[Main Effect] --> B[Service 1]
A --> C[Service 2]
B --> D[Service 3]
end
subgraph "Configuration"
E[Live Implementation] --> F[Config 1]
E --> G[Config 2]
end
subgraph "Runtime"
H[Provide Layer] --> A
E --> H
end
style A fill:#f9d6d2
style B fill:#f9d6d2
style C fill:#f9d6d2
style D fill:#f9d6d2
style E fill:#d2f9d6
style F fill:#d2f9d6
style G fill:#d2f9d6
style H fill:#d6d2f9
The biggest "aha" moment came when testing. With @effect/vitest
, testing effectful code becomes almost trivial:
it.effect("fetches and prints followers", () =>
Effect.gen(function* (_) {
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {})
console.log(mockFollowers.join("\n"))
try {
assert.strictEqual(consoleLogSpy.mock.calls.length, 1)
assert.strictEqual(consoleLogSpy.mock.calls[0][0], mockFollowers.join("\n"))
} finally {
consoleLogSpy.mockRestore()
}
})
)
While I'm enthusiastic about Effect, it's not the right solution for every problem:
Here's what I've realized: Effect isn't trying to replace TypeScript or make you learn a completely new language. It's extending what you already know by making effectful computations a first-class concept.
You don't need to discard your years of experience with JavaScript, TypeScript, and Node. Effect builds on them by using standard language features:
This is what Ethan meant when he called Effect a language—it's a way of expressing computations within TypeScript that gives you superpowers without forcing you to abandon your existing knowledge.
graph TD
A[TypeScript] --> B[Effect]
C[JavaScript] --> A
D[Node.js] --> E[Effect Platforms]
B --> F[Your Application]
E --> F
style A fill:#f9d6d2
style B fill:#d2f9d6
style C fill:#f9d6d2
style D fill:#f9d6d2
style E fill:#d2f9d6
style F fill:#d6d2f9
Whether you're building a tiny utility or a complex application, Effect provides a functional approach without the learning curve normally associated with purely functional languages.
Give it a try in your next project. You might find yourself thinking of TypeScript in a completely different way—I know I did.