Getting Started

Defining Errors

How each approach defines typed error classes.

Effect
import { Data } from 'effect'

class NotFoundError extends Data.TaggedError(
  'NotFoundError'
)<{ readonly id: string }> {}

class NetworkError extends Data.TaggedError(
  'NetworkError'
)<{ readonly url: string }> {}
import { Data } from 'effect'

class NotFoundError extends Data.TaggedError(
  'NotFoundError'
)<{ readonly id: string }> {}

class NetworkError extends Data.TaggedError(
  'NetworkError'
)<{ readonly url: string }> {}
errore
import * as errore from 'errore'

class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'User $id not found',
}) {}

class NetworkError extends errore.createTaggedError({
  name: 'NetworkError',
  message: 'Request to $url failed',
}) {}
import * as errore from 'errore'

class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'User $id not found',
}) {}

class NetworkError extends errore.createTaggedError({
  name: 'NetworkError',
  message: 'Request to $url failed',
}) {}

The Effect Type

Effect tracks three type parameters for every operation. errore uses a plain union.

Effect
import { Effect } from 'effect'

//         ┌── success
//         │      ┌── error
//         │      │         ┌── dependencies
//         ▼      ▼         ▼
// Effect< User,  HttpError, Database >
type GetUser = Effect.Effect<
  User,
  NotFoundError | NetworkError,
  Database
>

// Every function returns this 3-param type
function getUser(
  id: string
): Effect.Effect<
  User, NotFoundError | NetworkError, Database
>
import { Effect } from 'effect'

//         ┌── success
//         │      ┌── error
//         │      │         ┌── dependencies
//         ▼      ▼         ▼
// Effect< User,  HttpError, Database >
type GetUser = Effect.Effect<
  User,
  NotFoundError | NetworkError,
  Database
>

// Every function returns this 3-param type
function getUser(
  id: string
): Effect.Effect<
  User, NotFoundError | NetworkError, Database
>
errore
// Just a union: Error | Value
// No extra type parameters
function getUser(
  id: string
): Promise<NotFoundError | NetworkError | User>

// The return type tells the full story
const user = await getUser(id)
if (user instanceof NotFoundError) { /* ... */ }
if (user instanceof NetworkError) { /* ... */ }
console.log(user.name) // User
// Just a union: Error | Value
// No extra type parameters
function getUser(
  id: string
): Promise<NotFoundError | NetworkError | User>

// The return type tells the full story
const user = await getUser(id)
if (user instanceof NotFoundError) { /* ... */ }
if (user instanceof NetworkError) { /* ... */ }
console.log(user.name) // User

Running the Program

Every Effect program must be executed through a runtime. errore returns plain values — no runtime needed.

Effect
import { Effect } from 'effect'

// Nothing runs until you call a runner
const program = Effect.gen(function* () {
  const user = yield* fetchUser(id)
  const posts = yield* fetchPosts(user.id)
  return { user, posts }
})

// Choose your runner:
await Effect.runPromise(program)
Effect.runSync(program)
Effect.runFork(program)
Runtime.runPromise(customRuntime)(program)
import { Effect } from 'effect'

// Nothing runs until you call a runner
const program = Effect.gen(function* () {
  const user = yield* fetchUser(id)
  const posts = yield* fetchPosts(user.id)
  return { user, posts }
})

// Choose your runner:
await Effect.runPromise(program)
Effect.runSync(program)
Effect.runFork(program)
Runtime.runPromise(customRuntime)(program)
errore
// Just call the function. It returns a value.
const user = await fetchUser(id)
if (user instanceof Error) return user

const posts = await fetchPosts(user.id)
if (posts instanceof Error) return posts

// No runtime needed. Already done.
return { user, posts }
// Just call the function. It returns a value.
const user = await fetchUser(id)
if (user instanceof Error) return user

const posts = await fetchPosts(user.id)
if (posts instanceof Error) return posts

// No runtime needed. Already done.
return { user, posts }

Basic Error Handling

Fetching a user and handling a potential error.

Effect
import { Effect } from 'effect'

function getUser(id: string) {
  return Effect.gen(function* () {
    const user = yield* fetchUser(id)
    return user
  })
}

const result = Effect.runSync(
  getUser('123').pipe(
    Effect.catchTag('NotFoundError', (e) =>
      Effect.succeed(null)
    )
  )
)
import { Effect } from 'effect'

function getUser(id: string) {
  return Effect.gen(function* () {
    const user = yield* fetchUser(id)
    return user
  })
}

const result = Effect.runSync(
  getUser('123').pipe(
    Effect.catchTag('NotFoundError', (e) =>
      Effect.succeed(null)
    )
  )
)
errore
function getUser(
  id: string
): NotFoundError | User {
  const user = fetchUser(id)
  if (user instanceof NotFoundError) return user
  return user
}

const user = getUser('123')
if (user instanceof NotFoundError) {
  console.log('not found')
}
console.log(user.name)
function getUser(
  id: string
): NotFoundError | User {
  const user = fetchUser(id)
  if (user instanceof NotFoundError) return user
  return user
}

const user = getUser('123')
if (user instanceof NotFoundError) {
  console.log('not found')
}
console.log(user.name)

Error Handling

Catching Specific Errors

Selectively recovering from specific error types while letting others propagate.

Effect
import { Effect } from 'effect'

// catchTag — handle one specific error
const program = fetchUser(id).pipe(
  Effect.catchTag('NotFoundError', (e) =>
    Effect.succeed(
      { name: 'guest', id: e.id }
    )
  )
)
// NetworkError still propagates

// catchTags — handle multiple error types
const handled = fetchUser(id).pipe(
  Effect.catchTags({
    NotFoundError: (e) =>
      Effect.succeed({ name: 'guest', id: e.id }),
    NetworkError: (e) =>
      Effect.succeed(
        { name: 'offline', id: 'unknown' }
      )
  })
)

await Effect.runPromise(handled)
import { Effect } from 'effect'

// catchTag — handle one specific error
const program = fetchUser(id).pipe(
  Effect.catchTag('NotFoundError', (e) =>
    Effect.succeed(
      { name: 'guest', id: e.id }
    )
  )
)
// NetworkError still propagates

// catchTags — handle multiple error types
const handled = fetchUser(id).pipe(
  Effect.catchTags({
    NotFoundError: (e) =>
      Effect.succeed({ name: 'guest', id: e.id }),
    NetworkError: (e) =>
      Effect.succeed(
        { name: 'offline', id: 'unknown' }
      )
  })
)

await Effect.runPromise(handled)
errore
// Handle one specific error, let others propagate
const user = await fetchUser(id)
if (user instanceof NotFoundError) {
  return { name: 'guest', id: user.id }
}
if (user instanceof Error) return user
// NetworkError propagates, user is User here
// Handle one specific error, let others propagate
const user = await fetchUser(id)
if (user instanceof NotFoundError) {
  return { name: 'guest', id: user.id }
}
if (user instanceof Error) return user
// NetworkError propagates, user is User here

Pattern Matching

Exhaustive handling of all error cases.

Effect
import { Effect, Match } from 'effect'

const program = fetchUser(id).pipe(
  Effect.catchAll((error) =>
    Match.value(error).pipe(
      Match.tag('NotFoundError', (e) =>
        Effect.succeed(`User ${e.id} missing`)
      ),
      Match.tag('NetworkError', (e) =>
        Effect.succeed(`Failed: ${e.url}`)
      ),
      Match.exhaustive
    )
  )
)
import { Effect, Match } from 'effect'

const program = fetchUser(id).pipe(
  Effect.catchAll((error) =>
    Match.value(error).pipe(
      Match.tag('NotFoundError', (e) =>
        Effect.succeed(`User ${e.id} missing`)
      ),
      Match.tag('NetworkError', (e) =>
        Effect.succeed(`Failed: ${e.url}`)
      ),
      Match.exhaustive
    )
  )
)
errore
import * as errore from 'errore'

const user = await fetchUser(id)

if (user instanceof Error) {
  const message = errore.matchError(user, {
    NotFoundError: e => `User ${e.id} missing`,
    NetworkError: e => `Failed: ${e.url}`,
    Error: e => `Unexpected: ${e.message}`,
  })
  console.log(message)
}
import * as errore from 'errore'

const user = await fetchUser(id)

if (user instanceof Error) {
  const message = errore.matchError(user, {
    NotFoundError: e => `User ${e.id} missing`,
    NetworkError: e => `Failed: ${e.url}`,
    Error: e => `Unexpected: ${e.message}`,
  })
  console.log(message)
}

Short-Circuiting

When an error occurs in a chain of operations, all subsequent steps are skipped.

Effect
import { Effect, Console } from 'effect'

const task1 = Console.log('step 1...')
const task2 = Effect.fail(new NetworkError({
  url: '/api'
}))
const task3 = Console.log('step 3...')

const program = Effect.gen(function* () {
  yield* task1     // runs
  yield* task2     // fails — short circuits
  yield* task3     // never reached
})

// Output: "step 1..."
// Then fails with NetworkError
await Effect.runPromise(program)
import { Effect, Console } from 'effect'

const task1 = Console.log('step 1...')
const task2 = Effect.fail(new NetworkError({
  url: '/api'
}))
const task3 = Console.log('step 3...')

const program = Effect.gen(function* () {
  yield* task1     // runs
  yield* task2     // fails — short circuits
  yield* task3     // never reached
})

// Output: "step 1..."
// Then fails with NetworkError
await Effect.runPromise(program)
errore
console.log('step 1...')

const result = fetchData()
// Fails — early return, skip the rest
if (result instanceof Error) return result

// Never reached if fetchData failed
console.log('step 3...')
console.log('step 1...')

const result = fetchData()
// Fails — early return, skip the rest
if (result instanceof Error) return result

// Never reached if fetchData failed
console.log('step 3...')

Error Propagation

How errors flow through the call stack.

Effect
import { Effect } from 'effect'

function getUser(id: string): Effect.Effect<
  User,
  NotFoundError | NetworkError,
  never
>

const program = getUser('123').pipe(
  Effect.flatMap((user) =>
    getPosts(user.id)
  ),
  // Errors from both getUser and getPosts
  // accumulate in the channel type
  Effect.catchAll(handleError)
)
import { Effect } from 'effect'

function getUser(id: string): Effect.Effect<
  User,
  NotFoundError | NetworkError,
  never
>

const program = getUser('123').pipe(
  Effect.flatMap((user) =>
    getPosts(user.id)
  ),
  // Errors from both getUser and getPosts
  // accumulate in the channel type
  Effect.catchAll(handleError)
)
errore
function getUser(
  id: string
): NotFoundError | NetworkError | User

const user = getUser('123')
if (user instanceof Error) return user

const posts = getPosts(user.id)
if (posts instanceof Error) return posts
// TypeScript knows posts is Post[]
function getUser(
  id: string
): NotFoundError | NetworkError | User

const user = getUser('123')
if (user instanceof Error) return user

const posts = getPosts(user.id)
if (posts instanceof Error) return posts
// TypeScript knows posts is Post[]

Fallback Chain

Trying multiple strategies in sequence, falling back on failure.

Effect
import { Effect } from 'effect'

const program = fetchFromCache(id).pipe(
  Effect.orElse(() => fetchFromDb(id)),
  Effect.orElse(() => fetchFromApi(id)),
  Effect.catchAll(() =>
    Effect.succeed({
      name: 'Unknown',
      id
    })
  )
)

await Effect.runPromise(program)
import { Effect } from 'effect'

const program = fetchFromCache(id).pipe(
  Effect.orElse(() => fetchFromDb(id)),
  Effect.orElse(() => fetchFromApi(id)),
  Effect.catchAll(() =>
    Effect.succeed({
      name: 'Unknown',
      id
    })
  )
)

await Effect.runPromise(program)
errore
const cache = await fetchFromCache(id)
if (!(cache instanceof Error)) return cache

const db = await fetchFromDb(id)
if (!(db instanceof Error)) return db

const api = await fetchFromApi(id)
if (!(api instanceof Error)) return api

// All sources failed — return default
return { name: 'Unknown', id }
const cache = await fetchFromCache(id)
if (!(cache instanceof Error)) return cache

const db = await fetchFromDb(id)
if (!(db instanceof Error)) return db

const api = await fetchFromApi(id)
if (!(api instanceof Error)) return api

// All sources failed — return default
return { name: 'Unknown', id }

Error Accumulation

Collecting all errors instead of short-circuiting on the first failure.

Effect
import { Effect } from 'effect'

const program = Effect.forEach(
  userIds,
  (id) => fetchUser(id),
  { concurrency: 'unbounded' }
).pipe(
  Effect.validate,
  Effect.catchAll(([errors]) =>
    Effect.succeed({ errors, users: [] })
  )
)

// Or partition with Effect.partition
const [errors, users] = await Effect.runPromise(
  Effect.partition(
    userIds,
    (id) => fetchUser(id),
    { concurrency: 'unbounded' }
  )
)
import { Effect } from 'effect'

const program = Effect.forEach(
  userIds,
  (id) => fetchUser(id),
  { concurrency: 'unbounded' }
).pipe(
  Effect.validate,
  Effect.catchAll(([errors]) =>
    Effect.succeed({ errors, users: [] })
  )
)

// Or partition with Effect.partition
const [errors, users] = await Effect.runPromise(
  Effect.partition(
    userIds,
    (id) => fetchUser(id),
    { concurrency: 'unbounded' }
  )
)
errore
import * as errore from 'errore'

const results = await Promise.all(
  userIds.map((id) => fetchUser(id))
)

const [users, errors] = errore.partition(results)
// users: User[], errors: Error[]

errors.forEach((e) =>
  console.warn('Failed:', e.message)
)
import * as errore from 'errore'

const results = await Promise.all(
  userIds.map((id) => fetchUser(id))
)

const [users, errors] = errore.partition(results)
// users: User[], errors: Error[]

errors.forEach((e) =>
  console.warn('Failed:', e.message)
)

Async, Retries & Timeouts

Async Operations

Handling async operations that can fail.

Effect
import { Effect } from 'effect'

const getUser = (id: string) =>
  Effect.tryPromise({
    try: () => fetch(`/api/users/${id}`)
      .then(r => r.json()),
    catch: () =>
      new NetworkError({ url: `/api/users/${id}` })
  })

const program = Effect.gen(function* () {
  const user = yield* getUser('123')
  return user
})

await Effect.runPromise(program)
import { Effect } from 'effect'

const getUser = (id: string) =>
  Effect.tryPromise({
    try: () => fetch(`/api/users/${id}`)
      .then(r => r.json()),
    catch: () =>
      new NetworkError({ url: `/api/users/${id}` })
  })

const program = Effect.gen(function* () {
  const user = yield* getUser('123')
  return user
})

await Effect.runPromise(program)
errore
import * as errore from 'errore'

async function getUser(
  id: string
): Promise<NetworkError | User> {
  const res = await fetch(`/api/users/${id}`)
    .catch((e) => new NetworkError({
      url: `/api/users/${id}`, cause: e
    }))
  if (res instanceof Error) return res

  const data = await (res.json() as Promise<User>)
    .catch((e) => new NetworkError({
      url: `/api/users/${id}`, cause: e
    }))
  return data
}

const user = await getUser('123')
if (user instanceof NetworkError) return user
console.log(user.name)
import * as errore from 'errore'

async function getUser(
  id: string
): Promise<NetworkError | User> {
  const res = await fetch(`/api/users/${id}`)
    .catch((e) => new NetworkError({
      url: `/api/users/${id}`, cause: e
    }))
  if (res instanceof Error) return res

  const data = await (res.json() as Promise<User>)
    .catch((e) => new NetworkError({
      url: `/api/users/${id}`, cause: e
    }))
  return data
}

const user = await getUser('123')
if (user instanceof NetworkError) return user
console.log(user.name)

Retrying with Backoff

Retrying a failing operation with exponential backoff and a maximum number of attempts.

Effect
import { Effect, Schedule } from 'effect'

const policy = Schedule.exponential('100 millis').pipe(
  Schedule.compose(Schedule.recurs(3)),
  Schedule.union(
    Schedule.spaced('5 seconds')
  )
)

const program = Effect.gen(function* () {
  const user = yield* Effect.retry(
    fetchUser(id),
    policy
  )
  return user
})

await Effect.runPromise(program)
import { Effect, Schedule } from 'effect'

const policy = Schedule.exponential('100 millis').pipe(
  Schedule.compose(Schedule.recurs(3)),
  Schedule.union(
    Schedule.spaced('5 seconds')
  )
)

const program = Effect.gen(function* () {
  const user = yield* Effect.retry(
    fetchUser(id),
    policy
  )
  return user
})

await Effect.runPromise(program)
errore
async function fetchWithRetry(
  id: string
): Promise<NetworkError | User> {
  for (let i = 0; i < 3; i++) {
    const user = await fetchUser(id)
    if (!(user instanceof Error)) return user
    await sleep(100 * 2 ** i)
  }
  return new NetworkError({ url: `/users/${id}` })
}

const user = await fetchWithRetry(id)
if (user instanceof Error) return user
console.log(user.name)
async function fetchWithRetry(
  id: string
): Promise<NetworkError | User> {
  for (let i = 0; i < 3; i++) {
    const user = await fetchUser(id)
    if (!(user instanceof Error)) return user
    await sleep(100 * 2 ** i)
  }
  return new NetworkError({ url: `/users/${id}` })
}

const user = await fetchWithRetry(id)
if (user instanceof Error) return user
console.log(user.name)

Retry Until Condition

Retrying until a specific error condition is met, with different handling for the final error.

Effect
import { Effect } from 'effect'

const program = Effect.retry(
  fetchUser(id),
  {
    times: 5,
    until: (err) =>
      err._tag === 'NotFoundError'
  }
)

// Or with retryOrElse for a fallback
const withFallback = Effect.retryOrElse(
  fetchUser(id),
  Schedule.recurs(3),
  (error, _) =>
    Effect.succeed(
      { name: 'guest', id: 'unknown' }
    )
)

await Effect.runPromise(withFallback)
import { Effect } from 'effect'

const program = Effect.retry(
  fetchUser(id),
  {
    times: 5,
    until: (err) =>
      err._tag === 'NotFoundError'
  }
)

// Or with retryOrElse for a fallback
const withFallback = Effect.retryOrElse(
  fetchUser(id),
  Schedule.recurs(3),
  (error, _) =>
    Effect.succeed(
      { name: 'guest', id: 'unknown' }
    )
)

await Effect.runPromise(withFallback)
errore
async function fetchWithRetry(
  id: string
): Promise<NotFoundError | NetworkError | User> {
  for (let i = 0; i < 5; i++) {
    const user = await fetchUser(id)
    // Don't retry if it's a NotFoundError
    if (user instanceof NotFoundError) return user
    if (!(user instanceof Error)) return user
  }
  return new NetworkError({ url: `/users/${id}` })
}

// Or with a fallback on exhaustion
const user = await fetchWithRetry(id)
const result = user instanceof Error
  ? { name: 'guest', id: 'unknown' }
  : user
async function fetchWithRetry(
  id: string
): Promise<NotFoundError | NetworkError | User> {
  for (let i = 0; i < 5; i++) {
    const user = await fetchUser(id)
    // Don't retry if it's a NotFoundError
    if (user instanceof NotFoundError) return user
    if (!(user instanceof Error)) return user
  }
  return new NetworkError({ url: `/users/${id}` })
}

// Or with a fallback on exhaustion
const user = await fetchWithRetry(id)
const result = user instanceof Error
  ? { name: 'guest', id: 'unknown' }
  : user

Timeout

Aborting an operation if it takes too long and returning a typed error.

Effect
import { Effect } from 'effect'

const program = fetchUser(id).pipe(
  Effect.timeoutFail({
    duration: '5 seconds',
    onTimeout: () => new TimeoutError({
      operation: 'fetchUser',
      duration: '5s'
    })
  })
)

// The error channel now includes TimeoutError
const result = await Effect.runPromise(
  program.pipe(
    Effect.catchTag('TimeoutError', (e) =>
      Effect.succeed(null)
    )
  )
)
import { Effect } from 'effect'

const program = fetchUser(id).pipe(
  Effect.timeoutFail({
    duration: '5 seconds',
    onTimeout: () => new TimeoutError({
      operation: 'fetchUser',
      duration: '5s'
    })
  })
)

// The error channel now includes TimeoutError
const result = await Effect.runPromise(
  program.pipe(
    Effect.catchTag('TimeoutError', (e) =>
      Effect.succeed(null)
    )
  )
)
errore
import * as errore from 'errore'

async function fetchWithTimeout(
  id: string
): Promise<NetworkError | User> {
  const controller = new AbortController()
  const timer = setTimeout(
    () => controller.abort(), 5000
  )

  const user = await fetchUser(id, {
    signal: controller.signal
  }).catch((e) => new NetworkError({
    url: `/users/${id}`, cause: e
  }))
  clearTimeout(timer)

  if (user instanceof Error) return user
  return user
}
import * as errore from 'errore'

async function fetchWithTimeout(
  id: string
): Promise<NetworkError | User> {
  const controller = new AbortController()
  const timer = setTimeout(
    () => controller.abort(), 5000
  )

  const user = await fetchUser(id, {
    signal: controller.signal
  }).catch((e) => new NetworkError({
    url: `/users/${id}`, cause: e
  }))
  clearTimeout(timer)

  if (user instanceof Error) return user
  return user
}

Parallel Operations

Running multiple operations concurrently and handling individual failures.

Effect
import { Effect } from 'effect'

const program = Effect.all([
  fetchUser(id),
  fetchPosts(id),
  fetchStats(id),
], { concurrency: 'unbounded' })

// All succeed or the first error propagates
await Effect.runPromise(program)
import { Effect } from 'effect'

const program = Effect.all([
  fetchUser(id),
  fetchPosts(id),
  fetchStats(id),
], { concurrency: 'unbounded' })

// All succeed or the first error propagates
await Effect.runPromise(program)
errore
const [user, posts, stats] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchStats(id),
])

// Check each result individually
if (user instanceof Error) return user
if (posts instanceof Error) return posts
if (stats instanceof Error) return stats

return { user, posts, stats }
const [user, posts, stats] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchStats(id),
])

// Check each result individually
if (user instanceof Error) return user
if (posts instanceof Error) return posts
if (stats instanceof Error) return stats

return { user, posts, stats }

Cancellation & Cleanup

Interruption

Cancelling a running operation from the outside using fibers.

Effect
import { Effect, Fiber } from 'effect'

const program = Effect.gen(function* () {
  // Fork a long-running task into a fiber
  const fiber = yield* Effect.fork(longRunningTask)

  // Do other work...
  yield* doSomethingElse()

  // Cancel the fiber if still running
  yield* Fiber.interrupt(fiber)
})

// Or race two effects — loser gets interrupted
const fastest = Effect.race(
  fetchFromPrimary(id),
  fetchFromReplica(id)
)

await Effect.runPromise(fastest)
import { Effect, Fiber } from 'effect'

const program = Effect.gen(function* () {
  // Fork a long-running task into a fiber
  const fiber = yield* Effect.fork(longRunningTask)

  // Do other work...
  yield* doSomethingElse()

  // Cancel the fiber if still running
  yield* Fiber.interrupt(fiber)
})

// Or race two effects — loser gets interrupted
const fastest = Effect.race(
  fetchFromPrimary(id),
  fetchFromReplica(id)
)

await Effect.runPromise(fastest)
errore
// AbortController replaces fibers
const controller = new AbortController()
const task = longRunningTask(controller.signal)

// Do other work...
await doSomethingElse()

// Cancel the task
controller.abort()

// Or race two operations — first wins
const fastest = await Promise.race([
  fetchFromPrimary(id),
  fetchFromReplica(id),
])
if (fastest instanceof Error) return fastest
// AbortController replaces fibers
const controller = new AbortController()
const task = longRunningTask(controller.signal)

// Do other work...
await doSomethingElse()

// Cancel the task
controller.abort()

// Or race two operations — first wins
const fastest = await Promise.race([
  fetchFromPrimary(id),
  fetchFromReplica(id),
])
if (fastest instanceof Error) return fastest

Ensuring Cleanup on Interruption

Guaranteeing resource cleanup even when an operation is cancelled or interrupted.

Effect
import { Effect } from 'effect'

const withConnection = Effect.acquireRelease(
  Effect.sync(() => {
    const conn = createConnection()
    console.log('opened')
    return conn
  }),
  (conn) => Effect.sync(() => {
    conn.close()
    console.log('closed')
  })
)

const program = Effect.scoped(
  Effect.gen(function* () {
    const conn = yield* withConnection
    const data = yield* query(conn, sql)
    return data
  })
)

// If interrupted, the connection is still closed
await Effect.runPromise(program)
import { Effect } from 'effect'

const withConnection = Effect.acquireRelease(
  Effect.sync(() => {
    const conn = createConnection()
    console.log('opened')
    return conn
  }),
  (conn) => Effect.sync(() => {
    conn.close()
    console.log('closed')
  })
)

const program = Effect.scoped(
  Effect.gen(function* () {
    const conn = yield* withConnection
    const data = yield* query(conn, sql)
    return data
  })
)

// If interrupted, the connection is still closed
await Effect.runPromise(program)
errore
import * as errore from 'errore'

async function queryDb(
  sql: string
): Promise<DbError | Row[]> {
  await using cleanup = new errore.AsyncDisposableStack()

  const conn = createConnection()
  console.log('opened')
  cleanup.defer(() => {
    conn.close()
    console.log('closed')
  })

  // If anything fails, connection is still closed
  return query(conn, sql)
    .catch((e) => new DbError({ cause: e }))
}

const data = await queryDb(sql)
if (data instanceof Error) return data
import * as errore from 'errore'

async function queryDb(
  sql: string
): Promise<DbError | Row[]> {
  await using cleanup = new errore.AsyncDisposableStack()

  const conn = createConnection()
  console.log('opened')
  cleanup.defer(() => {
    conn.close()
    console.log('closed')
  })

  // If anything fails, connection is still closed
  return query(conn, sql)
    .catch((e) => new DbError({ cause: e }))
}

const data = await queryDb(sql)
if (data instanceof Error) return data

Finalization (ensuring / onExit)

Guaranteeing a cleanup step runs regardless of success, failure, or interruption.

Effect
import { Effect, Console } from 'effect'

// ensuring: cleanup runs on success, failure,
// and interruption
const program = Effect.gen(function* () {
  const data = yield* fetchData()
  return data
}).pipe(
  Effect.ensuring(
    Console.log('Cleanup completed')
  )
)

// onExit: cleanup receives the Exit value
const withExit = Effect.gen(function* () {
  const data = yield* fetchData()
  return data
}).pipe(
  Effect.onExit((exit) =>
    Console.log(`Exit: ${exit._tag}`)
  )
)

await Effect.runPromise(program)
import { Effect, Console } from 'effect'

// ensuring: cleanup runs on success, failure,
// and interruption
const program = Effect.gen(function* () {
  const data = yield* fetchData()
  return data
}).pipe(
  Effect.ensuring(
    Console.log('Cleanup completed')
  )
)

// onExit: cleanup receives the Exit value
const withExit = Effect.gen(function* () {
  const data = yield* fetchData()
  return data
}).pipe(
  Effect.onExit((exit) =>
    Console.log(`Exit: ${exit._tag}`)
  )
)

await Effect.runPromise(program)
errore
import * as errore from 'errore'

// await using = cleanup runs on every exit path
async function getData(): Promise<FetchError | Data> {
  await using cleanup =
    new errore.AsyncDisposableStack()

  cleanup.defer(() =>
    console.log('Cleanup completed')
  )

  const data = await fetchData()
    .catch((e) => new FetchError({ cause: e }))
  return data
  // cleanup runs automatically
}
import * as errore from 'errore'

// await using = cleanup runs on every exit path
async function getData(): Promise<FetchError | Data> {
  await using cleanup =
    new errore.AsyncDisposableStack()

  cleanup.defer(() =>
    console.log('Cleanup completed')
  )

  const data = await fetchData()
    .catch((e) => new FetchError({ cause: e }))
  return data
  // cleanup runs automatically
}

Scoped Finalizers (addFinalizer)

Registering cleanup actions within a scope that execute when the scope closes — regardless of how it closes.

Effect
import { Effect, Console } from 'effect'

const program = Effect.gen(function* () {
  yield* Effect.addFinalizer((exit) =>
    Console.log(
      `Finalizer: ${exit._tag}`
    )
  )
  const data = yield* fetchData()
  return data
})

// Must wrap in Effect.scoped to provide the Scope
const runnable = Effect.scoped(program)

await Effect.runPromise(runnable)
// Output: Finalizer: Success
import { Effect, Console } from 'effect'

const program = Effect.gen(function* () {
  yield* Effect.addFinalizer((exit) =>
    Console.log(
      `Finalizer: ${exit._tag}`
    )
  )
  const data = yield* fetchData()
  return data
})

// Must wrap in Effect.scoped to provide the Scope
const runnable = Effect.scoped(program)

await Effect.runPromise(runnable)
// Output: Finalizer: Success
errore
import * as errore from 'errore'

async function getData(): Promise<FetchError | Data> {
  await using cleanup =
    new errore.AsyncDisposableStack()

  cleanup.defer(() =>
    console.log('Finalizer: done')
  )

  const data = await fetchData()
    .catch((e) => new FetchError({ cause: e }))
  return data
  // "Finalizer: done" runs on every exit path
}
import * as errore from 'errore'

async function getData(): Promise<FetchError | Data> {
  await using cleanup =
    new errore.AsyncDisposableStack()

  cleanup.defer(() =>
    console.log('Finalizer: done')
  )

  const data = await fetchData()
    .catch((e) => new FetchError({ cause: e }))
  return data
  // "Finalizer: done" runs on every exit path
}

Multiple Resources with Defer

Managing multiple resources where cleanup order matters — each resource must be released even if earlier cleanup fails.

Effect
import { Effect } from 'effect'

const withDb = Effect.acquireRelease(
  Effect.promise(() => connectDb()),
  (db) => Effect.promise(() => db.close())
)
const withCache = Effect.acquireRelease(
  Effect.promise(() => openCache()),
  (cache) => Effect.promise(() => cache.flush())
)

const program = Effect.scoped(
  Effect.gen(function* () {
    const db = yield* withDb
    const cache = yield* withCache
    const order = yield* Effect.tryPromise({
      try: () => db.query(orderId),
      catch: () => new DbError({ orderId })
    })
    yield* Effect.promise(
      () => cache.set(orderId, order)
    )
    return order
  })
)

await Effect.runPromise(program)
import { Effect } from 'effect'

const withDb = Effect.acquireRelease(
  Effect.promise(() => connectDb()),
  (db) => Effect.promise(() => db.close())
)
const withCache = Effect.acquireRelease(
  Effect.promise(() => openCache()),
  (cache) => Effect.promise(() => cache.flush())
)

const program = Effect.scoped(
  Effect.gen(function* () {
    const db = yield* withDb
    const cache = yield* withCache
    const order = yield* Effect.tryPromise({
      try: () => db.query(orderId),
      catch: () => new DbError({ orderId })
    })
    yield* Effect.promise(
      () => cache.set(orderId, order)
    )
    return order
  })
)

await Effect.runPromise(program)
errore
import * as errore from 'errore'

async function processOrder(
  orderId: string
): Promise<DbError | CacheError | Order> {
  await using cleanup =
    new errore.AsyncDisposableStack()

  const db = await connectDb()
    .catch((e) => new DbError({ orderId, cause: e }))
  if (db instanceof Error) return db
  cleanup.defer(() => db.close())

  const cache = await openCache()
    .catch((e) =>
      new CacheError({ orderId, cause: e }))
  if (cache instanceof Error) return cache
  cleanup.defer(() => cache.flush())

  const order = await db.query(orderId)
    .catch((e) => new DbError({ orderId, cause: e }))
  if (order instanceof Error) return order

  await cache.set(orderId, order)
  return order
  // cleanup: cache.flush() → db.close()
}
import * as errore from 'errore'

async function processOrder(
  orderId: string
): Promise<DbError | CacheError | Order> {
  await using cleanup =
    new errore.AsyncDisposableStack()

  const db = await connectDb()
    .catch((e) => new DbError({ orderId, cause: e }))
  if (db instanceof Error) return db
  cleanup.defer(() => db.close())

  const cache = await openCache()
    .catch((e) =>
      new CacheError({ orderId, cause: e }))
  if (cache instanceof Error) return cache
  cleanup.defer(() => cache.flush())

  const order = await db.query(orderId)
    .catch((e) => new DbError({ orderId, cause: e }))
  if (order instanceof Error) return order

  await cache.set(orderId, order)
  return order
  // cleanup: cache.flush() → db.close()
}

Timeout with Resource Cleanup

Aborting an operation after a deadline while ensuring resources are released.

Effect
import { Effect } from 'effect'

const program = Effect.scoped(
  Effect.gen(function* () {
    const conn = yield* acquireConnection
    yield* Effect.addFinalizer(() =>
      Effect.promise(() => conn.close())
    )
    return yield* Effect.tryPromise(
      () => conn.query(sql)
    )
  })
).pipe(
  Effect.timeoutFail({
    duration: '5 seconds',
    onTimeout: () => new TimeoutError({
      operation: 'query'
    })
  })
)

await Effect.runPromise(program)
import { Effect } from 'effect'

const program = Effect.scoped(
  Effect.gen(function* () {
    const conn = yield* acquireConnection
    yield* Effect.addFinalizer(() =>
      Effect.promise(() => conn.close())
    )
    return yield* Effect.tryPromise(
      () => conn.query(sql)
    )
  })
).pipe(
  Effect.timeoutFail({
    duration: '5 seconds',
    onTimeout: () => new TimeoutError({
      operation: 'query'
    })
  })
)

await Effect.runPromise(program)
errore
import * as errore from 'errore'

async function queryWithTimeout(
  sql: string
): Promise<DbError | Row[]> {
  await using cleanup =
    new errore.AsyncDisposableStack()

  // AbortController for cancellation
  const controller = new AbortController()
  const timer = setTimeout(
    () => controller.abort(), 5000
  )
  cleanup.defer(() => clearTimeout(timer))

  const conn = await connect({
    signal: controller.signal
  }).catch((e) => new DbError({ cause: e }))
  if (conn instanceof Error) return conn
  cleanup.defer(() => conn.close())

  return conn.query(sql)
    .catch((e) => new DbError({ cause: e }))
  // caller uses errore.isAbortError() to detect timeout
  // cleanup: conn.close() → clearTimeout()
}
import * as errore from 'errore'

async function queryWithTimeout(
  sql: string
): Promise<DbError | Row[]> {
  await using cleanup =
    new errore.AsyncDisposableStack()

  // AbortController for cancellation
  const controller = new AbortController()
  const timer = setTimeout(
    () => controller.abort(), 5000
  )
  cleanup.defer(() => clearTimeout(timer))

  const conn = await connect({
    signal: controller.signal
  }).catch((e) => new DbError({ cause: e }))
  if (conn instanceof Error) return conn
  cleanup.defer(() => conn.close())

  return conn.query(sql)
    .catch((e) => new DbError({ cause: e }))
  // caller uses errore.isAbortError() to detect timeout
  // cleanup: conn.close() → clearTimeout()
}

Architecture

Composing Operations

Chaining multiple fallible operations together.

Effect
import { Effect } from 'effect'

const program = Effect.gen(function* () {
  const user = yield* fetchUser(id)
  const posts = yield* fetchPosts(user.id)
  const enriched = yield* enrichPosts(posts)
  return enriched
})

const result = await Effect.runPromise(
  program.pipe(
    Effect.catchTag('NotFoundError', () =>
      Effect.succeed([])
    ),
    Effect.catchTag('NetworkError', () =>
      Effect.succeed([])
    )
  )
)
import { Effect } from 'effect'

const program = Effect.gen(function* () {
  const user = yield* fetchUser(id)
  const posts = yield* fetchPosts(user.id)
  const enriched = yield* enrichPosts(posts)
  return enriched
})

const result = await Effect.runPromise(
  program.pipe(
    Effect.catchTag('NotFoundError', () =>
      Effect.succeed([])
    ),
    Effect.catchTag('NetworkError', () =>
      Effect.succeed([])
    )
  )
)
errore
const user = await fetchUser(id)
if (user instanceof NotFoundError) return []
if (user instanceof NetworkError) return []

const posts = await fetchPosts(user.id)
if (posts instanceof NetworkError) return []

const enriched = await enrichPosts(posts)
if (enriched instanceof Error) return []

return enriched
const user = await fetchUser(id)
if (user instanceof NotFoundError) return []
if (user instanceof NetworkError) return []

const posts = await fetchPosts(user.id)
if (posts instanceof NetworkError) return []

const enriched = await enrichPosts(posts)
if (enriched instanceof Error) return []

return enriched

Dependency Injection

Effect requires Context.Tag, Layer, and provideService to manage dependencies. errore uses plain function parameters.

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

class Database extends Context.Tag('Database')<
  Database,
  { query: (sql: string) => Effect.Effect<Row[]> }
>() {}

const program = Effect.gen(function* () {
  const db = yield* Database
  const rows = yield* db.query('SELECT * FROM users')
  return rows
})

// Must provide the service before running
const DatabaseLive = Layer.succeed(
  Database,
  {
    query: (sql) =>
      Effect.tryPromise(() =>
        pg.query(sql).then(r => r.rows)
      )
  }
)

const runnable = Effect.provide(
  program,
  DatabaseLive
)

await Effect.runPromise(runnable)
import { Effect, Context, Layer } from 'effect'

class Database extends Context.Tag('Database')<
  Database,
  { query: (sql: string) => Effect.Effect<Row[]> }
>() {}

const program = Effect.gen(function* () {
  const db = yield* Database
  const rows = yield* db.query('SELECT * FROM users')
  return rows
})

// Must provide the service before running
const DatabaseLive = Layer.succeed(
  Database,
  {
    query: (sql) =>
      Effect.tryPromise(() =>
        pg.query(sql).then(r => r.rows)
      )
  }
)

const runnable = Effect.provide(
  program,
  DatabaseLive
)

await Effect.runPromise(runnable)
errore
import * as errore from 'errore'

// Just pass the dependency as a parameter
async function getUsers(
  db: { query: (sql: string) => Promise<Row[]> }
): Promise<DbError | Row[]> {
  return db.query('SELECT * FROM users')
    .catch((e) => new DbError({ cause: e }))
}

// Call it directly with the real db
const rows = await getUsers(pg)
// Or in tests with a mock
const rows = await getUsers(mockDb)
import * as errore from 'errore'

// Just pass the dependency as a parameter
async function getUsers(
  db: { query: (sql: string) => Promise<Row[]> }
): Promise<DbError | Row[]> {
  return db.query('SELECT * FROM users')
    .catch((e) => new DbError({ cause: e }))
}

// Call it directly with the real db
const rows = await getUsers(pg)
// Or in tests with a mock
const rows = await getUsers(mockDb)

Wrapping Libraries That Throw

Converting exception-throwing code to typed errors.

Effect
import { Effect } from 'effect'

const parseConfig = (input: string) =>
  Effect.try({
    try: () => JSON.parse(input),
    catch: (e) => new ParseError({
      reason: String(e)
    })
  })

const program = Effect.gen(function* () {
  const config = yield* parseConfig(raw)
  return config
})
import { Effect } from 'effect'

const parseConfig = (input: string) =>
  Effect.try({
    try: () => JSON.parse(input),
    catch: (e) => new ParseError({
      reason: String(e)
    })
  })

const program = Effect.gen(function* () {
  const config = yield* parseConfig(raw)
  return config
})
errore
import * as errore from 'errore'

function parseConfig(
  input: string
): ParseError | Config {
  return errore.try({
    try: () => JSON.parse(input) as Config,
    catch: (e) => new ParseError({
      reason: e.message
    })
  })
}

const config = parseConfig(raw)
if (config instanceof ParseError) return config
console.log(config.dbUrl)
import * as errore from 'errore'

function parseConfig(
  input: string
): ParseError | Config {
  return errore.try({
    try: () => JSON.parse(input) as Config,
    catch: (e) => new ParseError({
      reason: e.message
    })
  })
}

const config = parseConfig(raw)
if (config instanceof ParseError) return config
console.log(config.dbUrl)

Library Authoring

Which approach is better for public APIs? Effect requires callers to install and learn the entire Effect ecosystem. errore uses plain TypeScript unions — zero new dependencies for your users.

Effect
import { Effect } from 'effect'

export function parse(
  input: string
): Effect.Effect<AST, ParseError> {
  // ...
}

// Callers need:
// npm install effect
// Learn Effect, pipe, gen, yield*
// 50+ modules in the effect ecosystem
import { Effect } from 'effect'

export function parse(
  input: string
): Effect.Effect<AST, ParseError> {
  // ...
}

// Callers need:
// npm install effect
// Learn Effect, pipe, gen, yield*
// 50+ modules in the effect ecosystem
errore
export function parse(
  input: string
): AST | ParseError {
  // ...
}

// Callers need:
// Nothing. Standard instanceof.
// No new concepts, no new deps.
// Works with any TypeScript project.
export function parse(
  input: string
): AST | ParseError {
  // ...
}

// Callers need:
// Nothing. Standard instanceof.
// No new concepts, no new deps.
// Works with any TypeScript project.