Mental model
Understand Result values, expected failures, defects, narrowing, and boundary design.
A Result<T, E> is a discriminated union:
class Ok<T, E = never> {
readonly status = "ok";
readonly value: T;
}
class Err<T, E> {
readonly status = "error";
readonly error: E;
}
type Result<T, E> = Ok<T, E> | Err<T, E>;
The actual classes carry methods and iterator behavior, but these three fields are the heart of the model.
Expected failures are values
Use Err<E> when a caller can make a meaningful decision:
- input is invalid;
- a record is absent;
- credentials are rejected;
- an upstream service is unavailable;
- serialization input does not satisfy a schema.
The caller sees E in the return type and cannot accidentally use the success value first.
Defects are not domain errors
A callback that unexpectedly throws inside .map(), .andThen(), .match(), recovery, observation, a generator, or codec validation becomes or throws a Panic. A defect should be reported and fixed, not silently widened into E | unknown.
const value = Result.ok(2).map(() => {
throw new Error("broken invariant");
});
// throws Panic
See Panic and defects for the exact boundary.
Branches narrow normally
const result: ResultType<User, FindUserError> = findUser(id);
if (result.status === "ok") {
result.value; // User
} else {
result.error; // FindUserError
}
You can also use Result.isOk, Result.isError, .isOk(), .isErr(), or .match().
Transform the branch you own
| Operation | Runs on | Purpose |
|---|---|---|
map |
Ok |
Change a success value |
mapError |
Err |
Change an error value |
andThen |
Ok |
Continue with another Result-returning step |
tryRecover |
Err |
Recover or replace the error |
tap / tapError |
selected branch | Observe without changing the Result |
match |
both | Leave the Result abstraction with one output |
Put Result at useful boundaries
A useful Result boundary has a caller that can act on the error. Good examples are repositories, parsers, domain operations, adapters, and application workflows. Do not wrap every pure getter or turn programmer mistakes into recoverable variants.
Composition preserves evidence
andThen and Result.gen union errors from every step. The final signature is a compact ledger of all expected failures:
const loadDashboard = (): ResultType<Dashboard, SessionExpired | UserNotFound | QueryFailed> =>
Result.gen(function* () {
const session = yield* readSession();
const user = yield* findUser(session.userId);
const dashboard = yield* queryDashboard(user.id);
return Result.ok(dashboard);
});
Handle or translate that union at the next meaningful boundary.