Overview

Catch Kind

Recover only one tagged domain error kind.

errorsrecovery

Install

$
Terminal window
pnpm dlx shadcn@latest add https://archyn.net/r/catch-kind.json

Copy the files below into the target paths shown in each tab.

ts@lib/flow/catch-kind.ts
import { type Awaitable, type Result, err } from "./core/result";
import { type Behavior, isRuntimeError, runBehavior } from "./core/runtime";
import { type TaskContext, task } from "./task";
type TaggedError<K extends PropertyKey = PropertyKey> = {
kind: K;
};
/**
* Recovers one discriminated domain error variant by its `kind` field.
*
* Matching errors are passed to the handler. Non-matching domain errors remain typed, and runtime
* errors bypass the handler.
*
* @typeParam I - Input type.
* @typeParam O - Source output type.
* @typeParam E - Tagged source domain error union.
* @typeParam C - Custom context type.
* @typeParam K - Error kind handled by this recovery step.
* @typeParam O2 - Handler output type.
* @typeParam E2 - Handler domain error type.
* @param source - Behavior whose domain error union should be narrowed.
* @param kind - Specific `kind` value to recover.
* @param handler - Callback invoked for matching domain errors.
* @returns A behavior with the handled error variant removed unless the handler returns a new error.
*
* @example
* ```ts
* const withDefault = catchKind(loadSettings, "missing", (_error, _input, context) =>
* context.ok({ theme: "system" }),
* );
* ```
*/
export function catchKind<
I,
O,
E extends TaggedError,
C extends object,
K extends E["kind"],
O2,
E2 = never,
>(
source: Behavior<I, O, E, C>,
kind: K,
handler: (
error: Extract<E, { kind: K }>,
input: I,
context: TaskContext<C>,
) => Awaitable<Result<O2, E2>>,
): Behavior<I, O | O2, Exclude<E, { kind: K }> | E2, C> {
return async (input, context) => {
const result = await runBehavior(source, input, context);
if (result.ok) {
return result;
}
if (isRuntimeError(result.error)) {
return err(result.error);
}
const error = result.error as E & TaggedError;
if (error.kind !== kind) {
return err(error as Exclude<E, { kind: K }>);
}
return task((_input: I, taskContext: TaskContext<C>) =>
handler(error as Extract<E, { kind: K }>, input, taskContext),
)(input, context);
};
}

Update the import paths to match your project setup.

Recover one tagged domain error kind while preserving the rest of the error union.

Usage

catchKind narrows a discriminated error union by kind. It handles one known error variant and keeps all unhandled variants typed.

Use it when:

  • Recover one specific error from a wider union.
  • Keep exhaustive error information for downstream code.
  • Avoid broad recovery handlers that hide unrelated domain failures.
import { catchKind } from "@/lib/flow/catch-kind";
import { task } from "@/lib/flow/task";
type LoadError = { kind: "missing" } | { kind: "forbidden" };
const loadSettings = task<string, { theme: string }, LoadError>((id, context) =>
id ? context.fail({ kind: "missing" }) : context.fail({ kind: "forbidden" }),
);
const loadSettingsWithDefault = catchKind(loadSettings, "missing", (_error, _id, context) =>
context.ok({ theme: "system" }),
);

Behavior

catchKind runs the source, checks failed domain errors for the requested kind, and delegates matching errors to the handler.

Non-matching domain errors pass through with their type preserved. Runtime errors and cancellation bypass the handler.

Types

The handled kind is removed from the source error union unless the handler returns a new error.

Import from @/lib/flow/catch-kind.

Composition Notes

Use catchKind for precise recovery from discriminated unions.

Do not use catchKind for untagged error values; recover is the general-purpose fallback.

Source

ts@lib/flow/catch-kind.ts
import { type Awaitable, type Result, err } from "./core/result";
import { type Behavior, isRuntimeError, runBehavior } from "./core/runtime";
import { type TaskContext, task } from "./task";
type TaggedError<K extends PropertyKey = PropertyKey> = {
kind: K;
};
/**
* Recovers one discriminated domain error variant by its `kind` field.
*
* Matching errors are passed to the handler. Non-matching domain errors remain typed, and runtime
* errors bypass the handler.
*
* @typeParam I - Input type.
* @typeParam O - Source output type.
* @typeParam E - Tagged source domain error union.
* @typeParam C - Custom context type.
* @typeParam K - Error kind handled by this recovery step.
* @typeParam O2 - Handler output type.
* @typeParam E2 - Handler domain error type.
* @param source - Behavior whose domain error union should be narrowed.
* @param kind - Specific `kind` value to recover.
* @param handler - Callback invoked for matching domain errors.
* @returns A behavior with the handled error variant removed unless the handler returns a new error.
*
* @example
* ```ts
* const withDefault = catchKind(loadSettings, "missing", (_error, _input, context) =>
* context.ok({ theme: "system" }),
* );
* ```
*/
export function catchKind<
I,
O,
E extends TaggedError,
C extends object,
K extends E["kind"],
O2,
E2 = never,
>(
source: Behavior<I, O, E, C>,
kind: K,
handler: (
error: Extract<E, { kind: K }>,
input: I,
context: TaskContext<C>,
) => Awaitable<Result<O2, E2>>,
): Behavior<I, O | O2, Exclude<E, { kind: K }> | E2, C> {
return async (input, context) => {
const result = await runBehavior(source, input, context);
if (result.ok) {
return result;
}
if (isRuntimeError(result.error)) {
return err(result.error);
}
const error = result.error as E & TaggedError;
if (error.kind !== kind) {
return err(error as Exclude<E, { kind: K }>);
}
return task((_input: I, taskContext: TaskContext<C>) =>
handler(error as Extract<E, { kind: K }>, input, taskContext),
)(input, context);
};
}