DEV Community

Alex Spinov
Alex Spinov

Posted on

Effect-TS Has a Free API That Makes TypeScript Error Handling Actually Work

Effect-TS is the missing standard library for TypeScript. Typed errors, dependency injection, concurrency, retries, and streaming — without external libraries.

What Is Effect?

Effect is a TypeScript library that gives you typed errors, composable services, structured concurrency, and resource management. Think of it as Rust's Result type meets Go's context, but in TypeScript.

Core Concept: Typed Errors

import { Effect, pipe } from 'effect'

class UserNotFound { readonly _tag = 'UserNotFound' as const }
class DatabaseError { readonly _tag = 'DatabaseError' as const }

// This function's type tells you EXACTLY what can go wrong
const getUser = (id: string): Effect.Effect<User, UserNotFound | DatabaseError> =>
  Effect.tryPromise({
    try: () => db.users.findUnique({ where: { id } }),
    catch: () => new DatabaseError(),
  }).pipe(
    Effect.flatMap((user) =>
      user ? Effect.succeed(user) : Effect.fail(new UserNotFound())
    )
  )

// Handle each error type differently
const program = getUser('123').pipe(
  Effect.catchTag('UserNotFound', () => Effect.succeed(defaultUser)),
  Effect.catchTag('DatabaseError', (e) => Effect.die(e)), // crash on DB errors
)
Enter fullscreen mode Exit fullscreen mode

Services (Dependency Injection)

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

// Define service interface
class EmailService extends Context.Tag('EmailService')<
  EmailService,
  { send: (to: string, body: string) => Effect.Effect<void> }
>() {}

class Logger extends Context.Tag('Logger')<
  Logger,
  { log: (msg: string) => Effect.Effect<void> }
>() {}

// Use services
const sendWelcome = (email: string) =>
  Effect.gen(function* () {
    const emailService = yield* EmailService
    const logger = yield* Logger
    yield* logger.log(`Sending welcome to ${email}`)
    yield* emailService.send(email, 'Welcome!')
  })

// Provide implementations
const EmailServiceLive = Layer.succeed(EmailService, {
  send: (to, body) => Effect.promise(() => resend.emails.send({ to, subject: 'Welcome', html: body })),
})

const LoggerLive = Layer.succeed(Logger, {
  log: (msg) => Effect.sync(() => console.log(msg)),
})

// Run with all dependencies
const MainLive = Layer.merge(EmailServiceLive, LoggerLive)
Effect.runPromise(sendWelcome('alice@example.com').pipe(Effect.provide(MainLive)))
Enter fullscreen mode Exit fullscreen mode

Concurrency

// Run 3 tasks in parallel
const results = yield* Effect.all(
  [fetchUser(1), fetchUser(2), fetchUser(3)],
  { concurrency: 3 }
)

// Run with bounded concurrency (max 5 at a time)
const allResults = yield* Effect.forEach(
  userIds,
  (id) => fetchUser(id),
  { concurrency: 5 }
)

// Race: first to succeed wins
const fastest = yield* Effect.race([
  fetchFromCDN(url),
  fetchFromOrigin(url),
])
Enter fullscreen mode Exit fullscreen mode

Retries

import { Schedule } from 'effect'

const retryPolicy = Schedule.exponential('100 millis').pipe(
  Schedule.compose(Schedule.recurs(3)),
)

const resilientFetch = fetchData.pipe(
  Effect.retry(retryPolicy),
  Effect.timeout('10 seconds'),
)
Enter fullscreen mode Exit fullscreen mode

Streaming

import { Stream } from 'effect'

const events = Stream.fromAsyncIterable(
  eventSource.subscribe('orders'),
  () => new Error('Stream failed')
).pipe(
  Stream.filter((e) => e.type === 'created'),
  Stream.map((e) => ({ orderId: e.id, total: e.amount })),
  Stream.grouped(10), // batch 10 events
  Stream.mapEffect((batch) => processBatch(batch)),
)

await Stream.runDrain(events)
Enter fullscreen mode Exit fullscreen mode

Why Effect?

  • Typed errors: know at compile time what can fail
  • Composable: pipe operations together
  • Testable: swap implementations via Layers
  • Concurrent: structured concurrency primitives
  • Resource-safe: automatic cleanup

Building resilient scraping pipelines? Scrapfly + Effect = typed, retryable data extraction. Email spinov001@gmail.com for robust scraping solutions.

Top comments (0)