Skip to main content

TS Ep 61: Introduction to Effect-TS

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 61: This Article
TypeScript gives you type safety, but it does not give you architectural safety. Effect-TS is the missing standard library that brings industrial-grade functional programming to the TypeScript ecosystem.

TL;DR (Quick Summary)
#

  • The Problem: TypeScript’s native Promise API is deeply flawed. It cannot track typed errors, it has no native retry mechanisms, and it provides zero support for Dependency Injection.
  • What is Effect?: Effect-TS is a fully-fledged functional standard library (comparable to ZIO in Scala). It provides immutable data structures, advanced error handling, concurrency primitives (Fibers), and robust telemetry.
  • The Mental Shift: In Effect, you never execute a side effect directly. You build a pipeline of instructions (Effect<A, E, R>), and hand that pipeline to a Runtime Engine to execute safely.
  • Getting Started: Install via npm install effect.

1. Introduction: The Evolution of TypeScript
#

TypeScript revolutionized the frontend and backend ecosystems by adding a robust, structural type system on top of JavaScript. It solved the “what is this object?” problem.

However, as Node.js and TypeScript scaled into massive enterprise microservices, developers realized that TypeScript lacked solutions for architectural safety.

Consider a standard asynchronous function in TypeScript today:

// 🔴 The standard TypeScript Promise
async function fetchUserData(userId: string): Promise<User> {
  const db = getDatabaseConnection(); // Where did this come from? (Global state)
  const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`);
  
  if (!user) {
    throw new Error("UserNotFound"); // The compiler ignores this!
  }
  
  return user;
}

This tiny snippet has three massive architectural flaws:

  1. Hidden Dependencies: getDatabaseConnection() relies on global state. Testing this requires mocking the entire database globally.
  2. Untyped Errors: The Promise<User> signature tells the caller absolutely nothing about the UserNotFound error.
  3. No Resilience: If the database connection blips, the function crashes immediately. Adding retries requires importing external libraries or writing messy while loops.

2. What is Effect-TS?
#

Effect-TS is an ecosystem designed to solve these exact problems. It is heavily inspired by ZIO (from the Scala ecosystem) and brings production-grade Functional Programming to TypeScript.

Effect is not just a utility library like Lodash; it is a complete replacement for the standard library. It provides:

  • The Effect Type: A hyper-charged, lazy alternative to Promise that tracks Success, Errors, and Context (Dependencies).
  • Advanced Data Structures: Immutable Option, Either, HashMap, and HashSet.
  • Concurrency: Lightweight “Fibers” (similar to Go routines) that replace heavy OS threads, allowing you to run millions of concurrent operations safely.
  • Telemetry: Built-in OpenTelemetry tracing and metrics.
  • Resource Management: Safe acquire/release patterns that guarantee database connections are closed, even if the program crashes.

3. Ecosystem Comparison
#

How does Effect compare to existing paradigms you might already know?

FeatureNative PromisesRxJS (Observables)Effect-TS
ExecutionEager (Runs immediately)Lazy (Runs on subscribe)Lazy (Runs explicitly via Runtime)
Error TypingNone (Throws any)None (Errors are any)Strict. Errors are part of the type signature.
Dependency InjectionManual / External LibraryManual / External LibraryNative. Tracked directly in the type signature.
ConcurrencyPromise.all (Basic)Advanced OperatorsFibers (Massively scalable, interruptible)
Retry LogicWrite it yourselfretryWhen (Complex)Effect.retry (Trivial, built-in schedules)

4. Step-by-Step: Your First Effect Program
#

Let’s dive in and write our first pure, functional program using Effect.

Step 1: Installation
#

First, initialize a new TypeScript project and install the effect package.

# Initialize project
npm init -y
npm install typescript @types/node tsx --save-dev
npx tsc --init

# Install Effect
npm install effect

Step 2: The Eager vs Lazy Mindset
#

In native JavaScript, when you call console.log, it executes immediately.

// Eager execution
console.log("Hello, World!"); // Prints instantly

In Effect, we use Effect.sync to wrap synchronous side effects. This creates a lazy description of the action.

import { Effect } from "effect";

// 🟢 Step 2: Create a lazy description
// This does NOT print anything to the console!
const program = Effect.sync(() => console.log("Hello, World!"));

If you run the script above, nothing happens. program is simply an immutable data structure (an Algebraic Data Type) that describes the intention to print to the console.

Step 3: Executing the Program
#

To actually perform the side effect, we must hand our description to the Effect Runtime Engine. At the absolute edge of your application (usually index.ts or main.ts), you run the program.

import { Effect } from "effect";

const program = Effect.sync(() => console.log("Hello, Effect!"));

// 🟢 Step 3: Execute the side effects
Effect.runSync(program); 
// Output: Hello, Effect!

5. Handling Asynchrony
#

What if the operation takes time, like fetching data from an API? We use Effect.promise.

import { Effect } from "effect";

// Create a lazy asynchronous description
const fetchTodo = Effect.promise(() => 
  fetch("https://jsonplaceholder.typicode.com/todos/1").then(res => res.json())
);

// We cannot use runSync for asynchronous effects! We must use runPromise.
Effect.runPromise(fetchTodo).then(console.log);

By wrapping our fetch call in Effect.promise, we have converted an eager Promise into a lazy, retryable Effect.


6. The Pipeline: Using Pipe
#

As we learned in Episode 60, we use the pipe function to string operations together. Effect provides thousands of utility functions that plug perfectly into pipe.

Let’s build a program that fetches a Todo, logs it, and adds a fallback.

import { Effect, Console } from "effect";

const program = Effect.pipe(
  // 1. Fetch the data
  Effect.promise(() => fetch("https://jsonplaceholder.typicode.com/todos/1")),
  
  // 2. Map the response to JSON
  Effect.flatMap(res => Effect.promise(() => res.json())),
  
  // 3. Log the title
  Effect.flatMap(data => Console.log(`Todo Title: ${data.title}`)),
  
  // 4. If anything fails, provide a fallback
  Effect.catchAll(() => Console.error("Failed to fetch Todo!"))
);

// Execute!
Effect.runPromise(program);

(Note: We will dive deep into flatMap, map, and catchAll in upcoming episodes. For now, just appreciate how cleanly the data flows from top to bottom!)


7. Troubleshooting & Common Errors
#

When setting up Effect for the first time, you might encounter some environment issues.

Error 1: Module Resolution Failure
#

Error: Cannot find module 'effect' or its corresponding type declarations.

The Cause: Your tsconfig.json is configured for legacy CommonJS environments, which struggles with modern package exports. The Fix: Ensure your tsconfig.json uses modern module resolution.

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "strict": true
  }
}

Error 2: Using runSync on Async Effects
#

Error: Fiber cannot be executed synchronously.

The Cause: You attempted to execute an asynchronous pipeline (like an HTTP request) using Effect.runSync(). The Fix: You must use Effect.runPromise() or Effect.runFork() to execute asynchronous pipelines.

// 🔴 Bad
Effect.runSync(Effect.promise(() => fetch("...")));

// 🟢 Good
Effect.runPromise(Effect.promise(() => fetch("...")));

Summary & Next Steps
#

In this episode:

  • We analyzed the architectural flaws of native TypeScript Promises.
  • We discovered how Effect-TS provides a robust standard library modeled on Functional principles.
  • We compared Effect to RxJS and Promises.
  • We installed Effect and wrote our first lazy, functional program.
  • We observed how pipe is used to string Effect operations together.

We keep mentioning that Effect tracks Success, Errors, and Dependencies. But how exactly does it do that?

In Episode 62: The Effect Type Signature, we will dissect the Effect<A, E, R> type and uncover the true superpower of this ecosystem!

typescript - This article is part of a series.
Part 61: This Article