better-effect
Core concepts

Pipelines

Compose Result programs with pipeable combinators.

Use pipe for ordinary unary function composition and the Effect combinators for Result-aware workflows. pipe deliberately knows nothing about Effect, Result, Promise, Scope or Service metadata.

pipe stays generic

import { pipe } from 'better-effect'

const label = pipe(
  '  ada  ',
  (value) => value.trim(),
  (value) => value.toUpperCase(),
  (value) => `USER:${value}`
)

It is useful for normal transformations, but it does not short-circuit or inspect Result values by itself.

Map success and errors

Effect.map and Effect.mapError support data-first and data-last forms:

const label = Effect.map(Result.ok(42), (value) => `#${value}`)
const readable = Effect.mapError(Result.err('missing'), (error) => ({ error }))

map transforms only Ok, and mapError transforms only Err. Both preserve the other channel, the synchronous/asynchronous shape and the phantom Service requirements.

Observe without changing a Result

Use the asynchronous tap wrappers for logging, metrics, or tracing in a pipeline:

const audited = pipe(
  loadUser(id),
  Effect.tapAsync((user) => metrics.recordLoaded(user.id)),
  Effect.tapErrorAsync((error) => metrics.recordFailed(error))
)

tapAsync, tapErrorAsync, and tapBothAsync always return a Promise. Their observers return PromiseLike<void>, so they preserve the source Effect's requirements rather than accepting an Effect callback with hidden Service resolution. better-result selects the active branch, retains the exact original Result when observation succeeds, and turns observer throws or rejections into its defect behavior.

Match tagged errors

Map tagged errors without dropping to raw Result.mapError:

const policy = Effect.matchError(loadUser(id), {
  UserNotFound: (error) => new HttpNotFound({ message: error.message }),
  DatabaseFailure: (error) => new HttpUnavailable({ message: error.message })
})

matchError is exhaustive for the known _tag union. For incremental policy, matchErrorPartial maps selected tags and carries unhandled variants forward in the Err type. Both wrappers delegate matching to better-result and retain source Service requirements.

Chain operations

Use Effect.andThen when the next function returns a synchronous Result:

const profile = pipe(
  findUser(id),
  Effect.andThen((user) => validateProfile(user)),
  Effect.map((profile) => profile.displayName)
)

The next function is skipped when the input is an Err. The output error type is the union of both operations' error types, and requirements are the union of both operations' requirements.

Use Effect.andThenAsync when the next operation is asynchronous:

const permissions = Effect.andThenAsync(findUser(id), (user) => loadPermissions(user.id))

andThenAsync always returns a Promise, including when the input is already an Err or a synchronous Result.

Choose a style

Requirements survive the pipeline

If loadUser requires UserRepository and loadPermissions requires PermissionService, the final pipeline requires both. The Runtime boundary sees that union even though the pipeline itself remains ordinary function composition.

On this page