Application patterns
Place Result boundaries, translate errors, compose workflows, and keep framework concerns at the edge.
Parse at the edge
Turn untrusted input into domain values once:
const parseCreateUser = (input: unknown): ResultType<CreateUser, InvalidCreateUser> => {
const parsed = CreateUserSchema.safeParse(input);
return parsed.success
? Result.ok(parsed.data)
: Result.err(new InvalidCreateUser({ issues: parsed.error.issues, message: "Invalid user" }));
};
Inner code should receive meaningful values instead of repeatedly checking unknown shapes.
One error vocabulary per boundary
A repository may expose UserNotFound | UserStoreUnavailable, while its database adapter knows driver-specific exceptions. Translate once:
const findUser = (id: UserId) =>
queryUserRow(id).mapError((cause) =>
cause._tag === "NoRows"
? new UserNotFound({ id, message: "User not found" })
: new UserStoreUnavailable({ cause, message: "User store unavailable" }),
);
Do not leak framework, database, or HTTP error types through domain APIs.
Compose in an application workflow
const registerUser = (input: unknown) =>
Result.gen(function* () {
const command = yield* parseCreateUser(input);
yield* ensureEmailAvailable(command.email);
const user = yield* insertUser(command);
yield* publishUserRegistered(user);
return Result.ok(user);
});
The signature records every expected failure. Handle or normalize the union where the caller’s policy lives.
Map to transport once
const toHttpResponse = (result: ResultType<User, RegisterUserError>) =>
result.match({
ok: (user) => Response.json(user, { status: 201 }),
err: (error) =>
matchError(error, {
InvalidCreateUser: (error) => Response.json(error.toJSON(), { status: 400 }),
EmailTaken: (error) => Response.json(error.toJSON(), { status: 409 }),
UserStoreUnavailable: () => Response.json({ message: "Try again" }, { status: 503 }),
PublishFailed: () => Response.json({ message: "Try again" }, { status: 503 }),
}),
});
Avoid common traps
String errors everywhere
Strings are hard to narrow, enrich, serialize deliberately, and match exhaustively. Prefer tagged classes at module boundaries.
Wrapping every function
Pure, total helpers do not need Result. Introduce Result where failure is part of the caller’s decision.
Catching every Panic
Panic is a defect signal. Catch it at reporting/supervision boundaries, not in ordinary domain control flow.
Unwrapping in the middle
unwrap() converts Err into a thrown defect. Compose or handle instead unless the Err truly
violates an invariant.