Skip to content
better-result
Esc
navigateopen⌘Jpreview
On this page

Matching errors

Handle tagged-error unions exhaustively or partially with inferred handler return unions.

Exhaustive instance matching

Every TaggedError instance has a .match() method:

const message = error.match({
  UserNotFound: (error) => `No user ${error.userId}`,
  PermissionDenied: (error) => `Missing permission ${error.permission}`,
});

Every _tag in the receiver union needs a handler. Handler parameters narrow to their exact error class. Different handler outputs produce a return union.

const outcome = error.match({
  UserNotFound: () => 404,
  PermissionDenied: () => "forbidden" as const,
});
// number | "forbidden"

This composes directly inside a Result error branch without annotating the inferred error:

const response = result.match({
  ok: (user) => ({ status: 200, body: user }),
  err: (error) =>
    error.match({
      UserNotFound: () => ({ status: 404, body: null }),
      DatabaseUnavailable: () => ({ status: 503, body: null }),
    }),
});

Standalone matchError

Use matchError(error, handlers) when working with structurally tagged errors that were not created by TaggedError:

const message = matchError(error, {
  UserNotFound: (error) => `No user ${error.userId}`,
  PermissionDenied: (error) => `Missing permission ${error.permission}`,
});

The standalone function has the same exhaustive narrowing, return inference, and defect behavior as .match(). If the selected exhaustive handler throws, .match() and matchError throw Panic with the original exception as cause.

match is a reserved TaggedError instance name. The factory rejects payload property types named match, preventing constructor assignment from shadowing the method.

Data-last matching

Annotate variant-specific parameters when the error union is not known until application:

const toMessage = matchError({
  UserNotFound: (error: UserNotFound) => `No user ${error.userId}`,
  PermissionDenied: (error: PermissionDenied) => error.message,
});

const message = toMessage(error);

Partial matching with identity fallback

matchErrorPartial returns unhandled errors unchanged by default:

const transformed = matchErrorPartial(error, {
  UserNotFound: (error) => `No user ${error.userId}`,
});
// string | PermissionDenied

This makes incremental handling explicit instead of discarding the rest of the union.

Custom fallback

const message = matchErrorPartial(
  error,
  { UserNotFound: (error) => `No user ${error.userId}` },
  (unhandled) => `Unexpected failure: ${unhandled.message}`,
);

Recover selected variants

Pass Result.err as the fallback to preserve unhandled variants while recovering one:

const recovered = result.tryRecover(
  matchErrorPartial(
    {
      UserNotFound: (_error: UserNotFound) => Result.ok(guestUser),
    },
    Result.err,
  ),
);

Match at boundaries

Keep domain functions returning typed errors. Match where the application can choose an HTTP status, CLI exit code, user message, retry policy, or compensating action.

Was this page helpful?