Result common patterns
Convert thrown boundaries once
Keep unknown exceptions at the edge of your application. Convert them into a tagged error before they enter typed business logic.
import { TaggedError, tryCatchAsync } from "@alt-stack/result";
class ProfileRequestError extends TaggedError {
readonly _tag = "ProfileRequestError" as const;
constructor(readonly cause: unknown) {
super("The profile request failed");
}
}
const result = await tryCatchAsync(
async () => {
const response = await fetch("https://example.test/profile");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<unknown>;
},
(cause) => new ProfileRequestError(cause),
);
Use tryCatch for synchronous functions, tryCatchAsync for a function that creates a promise, and fromPromise when you already have the promise.
Build a typed pipeline
import { flatMap, map, type Result } from "@alt-stack/result";
declare const parseUser: (input: unknown) => Result<User, ParseUserError>;
declare const authorize: (user: User) => Result<User, ForbiddenError>;
const label = map(
flatMap(parseUser(input), authorize),
(user) => `${user.id}:${user.name}`,
);
The returned error type is ParseUserError | ForbiddenError. Neither callback runs after the first Err.
Recover deliberately
catchError handles the original error and replaces it with the fallback's error type.
import { catchError, ok } from "@alt-stack/result";
const userOrGuest = catchError(findUser(id), () =>
ok({ id: "guest", name: "Guest" }),
);
Use mapError when the failure remains a failure but needs a different public shape—for example, translating an infrastructure error into a domain error.
Combine independent work
all returns the first error in input order or an Ok tuple containing every value.
import { all, ok } from "@alt-stack/result";
const combined = all([ok("Ada"), ok(37), ok(true)]);
// Result<[string, number, boolean], ...>
firstOk tries results in order. If none succeeds, it returns ResultAggregateError, whose errors property contains every failure.
import { err, firstOk } from "@alt-stack/result";
const selected = firstOk([
err(new PrimaryUnavailableError()),
err(new SecondaryUnavailableError()),
]);
These combinators consume already-computed results; they do not add concurrency or laziness.
Collapse at presentation boundaries
Use match when success and failure may produce different types, or fold when both handlers must return the same type.
import { match } from "@alt-stack/result";
const response = match(findUser(id), {
ok: (user) => ({ status: 200, body: user }),
err: (error) => ({ status: 404, body: { message: error.message } }),
});
Prefer unwrapOr, unwrapOrElse, or getOrUndefined for an intentional default. Reserve unwrap for a boundary where throwing the original error is the desired contract.
Observe without changing the value
tap and tapError return the original Result. They are useful for metrics or structured logs after the business decision has already been made.
import { tap, tapError } from "@alt-stack/result";
const observed = tapError(
tap(findUser(id), () => metrics.increment("user.lookup.ok")),
(error) => metrics.increment("user.lookup.error", { tag: error._tag }),
);
Keep callbacks synchronous and small. If observation can fail or must be awaited, model it as another explicit operation instead of hiding it inside tap.
Validate unknown errors
isResultError accepts only an actual Error instance with a string _tag. assertResultError enforces the same rule and throws TypeError when it fails.
import { err, isResultError } from "@alt-stack/result";
try {
await runPlugin();
} catch (error) {
return isResultError(error)
? err(error)
: err(new UnknownPluginError(error));
}
A plain object such as { _tag: "Failure" } is not a ResultError at runtime because it is not an Error instance.
See also
Result API Documentation lists exact signatures, properties, and type effects.