Skip to main content
@hex-di/result

Errors you can see.
Types you can trust.

Stop guessing what went wrong in catch blocks. Result makes errors visible, typed, and impossible to forget.

$ npm install @hex-di/result
inputok()mapandThenmatchonOkvalue ✓err()(skip)(skip)(skip)onErrerror ✗Ok path — values flow throughErr path — operations skipped
0
runtime dependencies
50+
API methods
7
combinators
TS 5.6+
TypeScript native

-- THE PROBLEM --

Try-catch gives you unknown. Result gives you types.

Traditional try-catch
try {
  const user = findUser(id);
  const order = createOrder(user);
  return order.confirmation;
} catch (e) {
  // e is `unknown` — what went wrong?
  // User not found? Order failed? DB down?
  console.error("something broke", e);
}
@hex-di/result
import { ok, err } from '@hex-di/result';

const result = findUser(id);
//    ^? Result<User, NotFoundError>

if (result.isOk()) {
  console.log(result.value.name);
  //                 ^? User — fully typed
} else {
  console.log(result.error._tag);
  //                 ^? NotFoundError — never unknown
}

:: quick start

Three steps. That's it.

1Install
npm install @hex-di/result
2Return a Result
return b === 0 ? err("division by zero") : ok(a / b);
3Handle both cases
result.match(v => use(v), e => handle(e));

:: familiar

You already know this

If you've used .map() on an array or .then() on a Promise, you already know how Result works.

JS you know

if (response.ok)

Result equivalent

if (result.isOk())

Same check — but the types narrow automatically.

JS you know

array.map(x => ...)

Result equivalent

result.map(x => ...)

Transform the value inside, skip if empty/error.

JS you know

promise.then(x => ...)

Result equivalent

result.andThen(x => ...)

Chain async-like steps — errors short-circuit.

:: features

Why Result?

No More Try-Catch

Errors are values, not exceptions. Pattern match on success and failure paths with full type safety.

Errors Skip Automatically

When something fails, the rest of the chain is skipped. No nested if-checks needed.

Never Forget an Error

TypeScript tells you at build time if you missed handling an error case.

Zero Runtime Cost

Lightweight wrapper with no dependencies. Result<T, E> compiles away to simple objects.

Handle Errors by Name

Each error has a name. Handle them one by one — TypeScript tracks which ones are left.

Reusable Error Handlers

Write an error handler once, apply it anywhere. Compose multiple handlers into one — TypeScript tracks what's left.

:: pipeline

Chain operations, not try-catch blocks

Build error-handling pipelines with a fluent API. Each step is type-checked. Errors short-circuit automatically.

1.
findUser()Returns Result<User, NotFound>
2.
andThen()Check the user is active
3.
map()Transform into a display profile
4.
match()Render success or show error
pipeline.ts
import { ok, err } from '@hex-di/result';

const profile = findUser(id)   // Result<User, NotFound>
  .andThen((user) =>
    user.active
      ? ok(user)
      : err({ _tag: "Inactive", id: user.id })
  )
  .map((user) => ({ ...user, displayName: formatName(user) }))
  .match(
    (profile) => renderProfile(profile),
    (e) => showError(e._tag),
  );

Start building type-safe error handling today

Replace try-catch with composable, typed pipelines. Zero dependencies. Full TypeScript inference.

:: going deeper

Master these patterns when you're ready.

:: api

50+ methods. One import.

Create results, chain operations, combine results, handle tagged errors — everything you need to stop throwing.

// constructors

ok / err

const a = ok(42);       // Ok<number>
const b = err('fail');  // Err<string>

Wrap values into the Result type

fromThrowable

const r = fromThrowable(
  () => JSON.parse(s),
  (e) => new ParseError(e),
);

Catch exceptions as typed errors

fromNullable

const r = fromNullable(
  map.get(key),
  () => new NotFound(key),
);

Convert null | undefined to Err

// chaining

map / mapErr

ok(2)
  .map(n => n * 10)     // Ok(20)
  .mapErr(e => wrap(e)) // skipped

Transform the Ok or Err value

andThen / orElse

ok(id)
  .andThen(id => findUser(id))
  .orElse(e => fallback(e))

Chain Result-returning functions

andTee / orTee

ok(user)
  .andTee(u => log('found', u))
  .orTee(e => report(e))

Side-effects without changing the value

// combinators

all()

const r = all(
  fetchUser(id),
  fetchOrder(id),
); // Ok<[User, Order]>

All must succeed or first Err wins

zipOrAccumulate()

const r = zipOrAccumulate(
  validateName(name),
  validateAge(age),
); // Err<[E, ...E[]]> collects ALL

Accumulate all errors, no short-circuit

partition()

const [oks, errs] = partition(
  items.map(i => validate(i))
);

Split into successes and failures

// tagged error handling

catchTag()

result
  .catchTag("NotFound", (e) =>
    ok(`Fallback: ${e.resource}`)
  ) // narrows error union

Handle one tagged error, narrow the type

catchTags()

result.catchTags({
  NotFound: (e) => ok("default"),
  Timeout:  (e) => ok("retry"),
}) // handles multiple at once

Handle multiple tagged errors in one call

createErrorGroup()

const Http = createErrorGroup("Http");
const NotFound = Http.create("NotFound");
const e = NotFound({ url: "/api", status: 404 });
Http.is(e) // true

Two-level discriminated error families

tagged-errors.ts
type PaymentError =
  | { _tag: "CardExpired"; card: string }
  | { _tag: "NetworkTimeout"; ms: number }
  | { _tag: "FraudDetected"; score: number };

const result: Result<string, PaymentError> = chargeCard(card);

const handled = result
  .catchTag("NetworkTimeout", (e) =>
    ok(`Retried after ${e.ms}ms`))
  // Type: Result<string, CardExpired | FraudDetected>

  .catchTag("CardExpired", (e) =>
    ok(`Renewed card ${e.card}`))
  // Type: Result<string, FraudDetected>

  .orTee((e) => alertFraudTeam(e));
  // Only FraudDetected remains — type proves it ✓

:: tagged errors

Eliminate errors one tag at a time

Use catchTag and catchTags to progressively handle errors by their discriminant. TypeScript narrows the union after each handler — until nothing remains.

catchTag()Handle one error type, narrow the union
catchTags()Handle multiple at once, narrow all
andThenWith()Chain with success + error recovery
orDie()Extract value or throw — for boundaries

:: generators

Write straight-line code that bails on errors

Use safeTry with generator functions to write linear code that stops at the first error. Each yield* unwraps an Ok value or immediately returns the Err. No nesting, no callbacks, no .then() chains.

safe-try.ts
import { safeTry, ok } from '@hex-di/result';

const approval = safeTry(function* () {
  const credit = yield* checkCredit(applicantId);
  const risk   = yield* assessRisk(credit);
  const terms  = yield* generateTerms(risk);

  return ok({ approved: true, terms });
  // If any step returns Err, execution stops
  // and the Err propagates automatically
});

:: accumulate

Build typed objects field by field

Use bind and let_ to accumulate fields into a typed object. Each step can fail — and the full object type is inferred automatically.

bind adds a Result-producing field. let_ adds a pure value. Both accumulate into the same typed record.

do-notation.ts
import { ok, bind, let_ } from '@hex-di/result';

const user = ok({})
  .andThen(bind("name", () => validateName("Alice")))
  .andThen(bind("email", () => validateEmail("a@ex.com")))
  .andThen(bind("age", () => validateAge(25)))
  .andThen(let_("id", () => `usr_${Date.now()}`));
// Type: Result<{
//   name: string; email: string;
//   age: number; id: string;
// }, ValidationError>

:: effect system

Composable error handlers

Write a handler for each error type, then snap them together. Apply the combined handler to any Result — TypeScript proves which errors are left. Zero runtime cost.

EffectHandlerOne handler for one error type
composeHandlersSnap multiple handlers together
transformEffectsApply combined handler to any Result
EffectContractDeclare which errors a function can produce
effect-handlers.ts
import {
  composeHandlers, transformEffects,
  type EffectHandler,
} from '@hex-di/result';

const quotaHandler: EffectHandler<QuotaExceeded, string> = {
  _tag: "quota", tags: ["QuotaExceeded"],
  handle: (e) => ok(`Queued: tenant ${e.tenantId}`),
};

const corruptionHandler: EffectHandler<FileCorrupted, string> = {
  _tag: "corruption", tags: ["FileCorrupted"],
  handle: (e) => ok(`Quarantined: ${e.fileName}`),
};

// Compose + apply — only PermissionDenied remains
const handler = composeHandlers(quotaHandler, corruptionHandler);
const result = transformEffects(processFile(file), handler);
// Type: Result<string, PermissionDenied>

:: ecosystem

Part of the HexDI stack

Result integrates seamlessly with HexDI adapters. Every factory returns Result<T, E> — errors are typed and composable across your entire dependency graph.

user-service.adapter.ts
import { createAdapter } from 'hex-di';
import { ok, fromThrowable } from '@hex-di/result';

const UserService = createAdapter({
  provides: UserServicePort,
  requires: [DatabasePort, LoggerPort],
  lifetime: 'singleton',
  factory: ({ Database, Logger }) => ({
    findUser: (id: string) =>
      fromThrowable(
        () => Database.query(id),
        () => new DatabaseError(id),
      )
        .andTee((u) => Logger.info('found', u))
        .mapErr((e) => new UserNotFound(id, e)),
  }),
});
hex-diDI Core
@hex-di/resultResult Type
@hex-di/flowState Machines
@hex-di/sagaOrchestration
@hex-di/guardAuth & Permissions
@hex-di/queryData Fetching

Start building type-safe error handling today

Replace try-catch with composable, typed pipelines. Zero dependencies. Full TypeScript inference.