← All articles
LANGUAGES & FRAMEWORKS Effect-TS: Building Robust TypeScript Applications w... 2026-02-15 · 5 min read · effect · typescript · functional-programming

Effect-TS: Building Robust TypeScript Applications with Typed Effects

Languages & Frameworks 2026-02-15 · 5 min read effect typescript functional-programming error-handling concurrency

Effect-TS: Building Robust TypeScript Applications with Typed Effects

Effect-TS is a TypeScript framework that brings the power of effect systems to JavaScript/TypeScript applications. If you've struggled with error handling, dependency injection, or concurrent operations in TypeScript, Effect provides a unified solution that makes these concerns explicit, composable, and type-safe.

Effect-TS framework logo

What is Effect?

At its core, Effect is a data type representing a computation that may:

The Effect<A, E, R> type signature tells you everything about what a computation needs, how it can fail, and what it produces.

import { Effect } from "effect"

// An effect that requires no dependencies (never),
// can fail with Error, and succeeds with a number
type UserCount = Effect.Effect<number, Error, never>

// An effect that requires a Database service,
// can fail with DbError, and succeeds with User[]
type GetUsers = Effect.Effect<User[], DbError, Database>

This is fundamentally different from Promise-based code, where errors are untyped and dependencies are implicit.

Typed Error Handling

JavaScript's Promise type only tracks the success value. Errors are untyped and often lost in type signatures. Effect makes errors explicit.

import { Effect, Data } from "effect"

// Define typed errors
class NetworkError extends Data.TaggedError("NetworkError")<{
  readonly url: string
  readonly cause: unknown
}> {}

class ParseError extends Data.TaggedError("ParseError")<{
  readonly body: string
  readonly reason: string
}> {}

// A function that can fail with specific errors
const fetchUser = (userId: string): Effect.Effect<User, NetworkError | ParseError> =>
  Effect.gen(function* () {
    // Try/catch is handled by Effect
    const response = yield* Effect.tryPromise({
      try: () => fetch(`/api/users/${userId}`),
      catch: (error) => new NetworkError({ url: `/api/users/${userId}`, cause: error })
    })

    const text = yield* Effect.promise(() => response.text())

    const user = yield* Effect.try({
      try: () => JSON.parse(text) as User,
      catch: () => new ParseError({ body: text, reason: "Invalid JSON" })
    })

    return user
  })

Callers can handle specific errors:

const program = fetchUser("123").pipe(
  Effect.catchTag("NetworkError", (error) =>
    Effect.succeed({ id: "123", name: "Fallback User" })
  ),
  Effect.catchTag("ParseError", (error) =>
    Effect.fail(new Error(`Parse failed: ${error.reason}`))
  )
)

Compare this to Promise-based code where you'd need runtime checks to distinguish error types.

Dependency Injection with Layers and Services

Effect's dependency injection system makes services and configuration explicit in types.

import { Context, Effect, Layer } from "effect"

// Define a service interface
interface Database {
  readonly query: (sql: string) => Effect.Effect<Row[], DbError>
}

// Create a service tag
const Database = Context.GenericTag<Database>("Database")

// Use the service
const getAllUsers = Effect.gen(function* () {
  const db = yield* Database
  const rows = yield* db.query("SELECT * FROM users")
  return rows.map(rowToUser)
})

// Type signature shows Database dependency
// Effect<User[], DbError, Database>

Provide implementations with Layers:

// Implementation for production
const DatabaseLive = Layer.succeed(
  Database,
  {
    query: (sql) => Effect.tryPromise({
      try: () => pool.query(sql),
      catch: (error) => new DbError({ sql, cause: error })
    })
  }
)

// Mock for testing
const DatabaseTest = Layer.succeed(
  Database,
  {
    query: (sql) => Effect.succeed([
      { id: 1, name: "Test User" }
    ])
  }
)

// Run with production layer
Effect.runPromise(
  getAllUsers.pipe(Effect.provide(DatabaseLive))
)

// Run with test layer
Effect.runPromise(
  getAllUsers.pipe(Effect.provide(DatabaseTest))
)

This is cleaner than constructor injection and more type-safe than ambient globals.

Concurrency with Fibers

Effect has a built-in concurrency model based on fibers (lightweight green threads).

import { Effect, Fiber } from "effect"

// Run effects concurrently
const parallelFetch = Effect.all([
  fetchUser("1"),
  fetchUser("2"),
  fetchUser("3")
], { concurrency: "unbounded" })

// Race multiple effects
const fastest = Effect.race(
  fetchFromPrimaryDB,
  fetchFromReplicaDB
)

// Run with timeout
const withTimeout = Effect.timeout(
  slowOperation,
  "5 seconds"
)

// Manual fiber control
const fiberProgram = Effect.gen(function* () {
  const fiber = yield* Effect.fork(longRunningTask)

  // Do other work...

  // Wait for fiber
  const result = yield* Fiber.join(fiber)

  return result
})

Compare this to juggling Promises, AbortControllers, and race conditions manually.

Streaming with Stream

Effect includes Stream, a composable abstraction for processing data over time.

import { Stream, Effect } from "effect"

// Create a stream from an array
const numbers = Stream.make(1, 2, 3, 4, 5)

// Transform streams
const doubled = numbers.pipe(
  Stream.map(n => n * 2),
  Stream.filter(n => n > 4)
)

// Consume a stream
const sum = Stream.runFold(doubled, 0, (acc, n) => acc + n)

// Stream from async source
const logStream = Stream.asyncEffect<LogEntry, never, never>((emit) =>
  Effect.gen(function* () {
    const subscription = logService.subscribe((entry) => {
      emit(Effect.succeed(Stream.make(entry)))
    })

    return Effect.sync(() => subscription.unsubscribe())
  })
)

// Process stream with backpressure
const processed = logStream.pipe(
  Stream.groupedWithin(100, "1 second"),
  Stream.mapEffect(batch => writeToDB(batch)),
  Stream.runDrain
)

Streams handle backpressure, resource cleanup, and error propagation automatically.

Comparison to Alternatives

vs. fp-ts

Effect is the spiritual successor to fp-ts. It shares functional programming principles but with better ergonomics:

vs. neverthrow

neverthrow provides Result<T, E> for synchronous error handling. Effect goes further:

If you only need sync error handling, neverthrow is simpler. For building systems, Effect provides more.

Real-World Example

Here's a complete HTTP endpoint using Effect:

import { Effect, Layer } from "effect"
import { HttpServer, HttpRouter } from "@effect/platform"

// Services
const Database = Context.GenericTag<Database>("Database")
const Cache = Context.GenericTag<Cache>("Cache")

// Business logic
const getUser = (id: string) =>
  Effect.gen(function* () {
    const cache = yield* Cache
    const cached = yield* cache.get(id).pipe(Effect.option)

    if (cached._tag === "Some") {
      return cached.value
    }

    const db = yield* Database
    const user = yield* db.query(`SELECT * FROM users WHERE id = $1`, [id])

    yield* cache.set(id, user, "5 minutes")

    return user
  })

// HTTP route
const userRoutes = HttpRouter.empty.pipe(
  HttpRouter.get("/users/:id", (req) =>
    Effect.gen(function* () {
      const { id } = req.params
      const user = yield* getUser(id)
      return HttpServer.response.json(user)
    }).pipe(
      Effect.catchAll((error) =>
        HttpServer.response.json({ error: error.message }, { status: 500 })
      )
    )
  )
)

// Application layer
const AppLayer = Layer.mergeAll(
  DatabaseLive,
  CacheLive
)

// Run server
const server = HttpServer.serve(userRoutes).pipe(
  Effect.provide(AppLayer),
  Effect.provide(HttpServer.layer({ port: 3000 }))
)

Effect.runFork(server)

This gives you:

Getting Started

Install Effect:

npm install effect

Start with the Effect type and generators:

import { Effect } from "effect"

const program = Effect.gen(function* () {
  const a = yield* Effect.succeed(10)
  const b = yield* Effect.succeed(20)
  return a + b
})

Effect.runPromise(program).then(console.log) // 30

Then layer in services, error handling, and concurrency as you need them.

When to Use Effect

Effect shines when:

Effect adds complexity. Use it when reliability and maintainability justify the learning curve.

Resources

Effect represents a different way of thinking about TypeScript applications. The upfront investment in learning pays off in reliability, testability, and maintainability.