| name | result-pattern |
| description | Type-safe error handling via Result<T>. Trigger: When handling expected business errors (validation, not found) without throwing exceptions. |
| license | Apache 2.0 |
| metadata | {"version":"1.0","type":"domain"} |
Result Pattern
Wraps operation outcomes in Result<T> representing success or failure. Alternative to throwing exceptions for expected errorsโprovides explicit error paths and forces consumers to handle errors.
When to Use
- Expected business errors (validation failed, user not found, unauthorized)
- Chaining multiple operations that can fail
- Type-safe error handling across layers
- API endpoints that need different HTTP status codes per error type
Don't use for:
- Truly unexpected errors (null pointer, out of memory) โ throw exceptions
- Simple getters that can't fail
- Internal private helpers
Critical Patterns
โ
REQUIRED: Basic Result
export class Result<T> {
private constructor(
public readonly isSuccess: boolean,
public readonly value?: T,
public readonly error?: string
) {}
static ok<T>(value: T): Result<T> { return new Result(true, value); }
static fail<T>(error: string): Result<T> { return new Result(false, undefined, error); }
flatMap<U>(fn: (value: T) => Result<U>): Result<U> {
return this.isSuccess ? fn(this.value!) : Result.fail<U>(this.error!);
}
}
function divide(a: number, b: number): Result<number> {
if (b === 0) return Result.fail("Cannot divide by zero");
return Result.ok(a / b);
}
const result = divide(10, 2);
if (result.isSuccess) console.log(result.value);
else console.error(result.error);
โ
REQUIRED: Chain with flatMap
function parseAge(s: string): Result<number> {
const n = parseInt(s);
return isNaN(n) ? Result.fail("Not a number") : Result.ok(n);
}
function validateAge(n: number): Result<number> {
return n >= 0 && n < 150 ? Result.ok(n) : Result.fail("Age out of range");
}
const result = parseAge("25").flatMap(validateAge);
if (result.isSuccess) console.log(result.value);
โ
REQUIRED: Service Layer Returns Result
class UserService {
async createUser(data: CreateUserDTO): Promise<Result<User>> {
if (!data.email.includes("@")) return Result.fail("Invalid email");
const existing = await this.repo.findByEmail(data.email);
if (existing) return Result.fail("Email already registered");
const user = await this.repo.create(data);
return Result.ok(user);
}
}
โ
REQUIRED: Controller Maps Result to HTTP
app.post("/users", async (req, res) => {
const result = await userService.createUser(req.body);
if (result.isSuccess) res.status(201).json(result.value);
else res.status(400).json({ error: result.error });
});
โ
REQUIRED: Frontend โ React Hook with Result
Use Result in hooks to surface typed errors without exceptions bubbling into components.
async function submitOrder(items: OrderItem[]): Promise<Result<Order>> {
if (items.length === 0) return Result.fail("EMPTY_ORDER");
const res = await fetch("/api/orders", { method: "POST", body: JSON.stringify({ items }) });
if (!res.ok) return Result.fail(res.status === 409 ? "ORDER_CONFLICT" : "SERVER_ERROR");
return Result.ok(await res.json());
}
function useCreateOrder() {
const [state, setState] = useState<"idle" | "loading" | "success" | "error">("idle");
const [errorCode, setErrorCode] = useState< | >();
(): <> {
();
result = (items);
(result.) { (); }
{ (); (result.!); }
}
{ submit, state, errorCode };
}
: <, > = {
: ,
: ,
: ,
};
โ NEVER: Swallow Errors Without Result
try { await createUser(data); } catch { }
const result = await createUser(data);
if (!result.isSuccess) handleError(result.error);
Decision Tree
Expected business error (validation, not found, unauthorized)?
โ Return Result.fail("message")
Programmer error (null pointer, wrong arg type)?
โ Throw exception (not Result)
Multiple operations that can fail sequentially?
โ Chain with flatMap, or check isSuccess at each step
API endpoint needs to return different HTTP codes per error?
โ Service returns Result โ controller maps Result to HTTP status
Need typed error variants (ValidationError, NotFoundError)?
โ Add a discriminated union error type to Result<T, E> โ see references/advanced-patterns.md
Operation may or may not return a value (nullable)?
โ Return Result<T | undefined> or use a dedicated wrapper โ see references/advanced-patterns.md
Example
End-to-end: service returns Result โ controller maps each failure to the correct HTTP status code.
class OrderService {
async placeOrder(userId: string, dto: PlaceOrderDTO): Promise<Result<Order>> {
const user = await this.userRepo.findById(userId);
if (!user) return Result.fail("USER_NOT_FOUND");
if (!user.isActive) return Result.fail("USER_INACTIVE");
if (dto.items.length === 0) return Result.fail("EMPTY_ORDER");
const order = Order.create(userId, dto.items);
await this.orderRepo.save(order);
return Result.ok(order);
}
}
app.post("/api/v1/orders", (req, res) => {
result = orderService.(req.., req.);
(result.) {
res.().(result.);
}
: <, > = {
: ,
: ,
: ,
};
status = statusMap[result.!] ?? ;
res.(status).({ : result. });
});
Patterns applied: service returns Result.ok / Result.fail, error codes are plain strings the controller maps to HTTP statuses, no try/catch needed โ all paths are explicit.
Edge Cases
Team unfamiliarity: Result pattern has a learning curve. If team is unfamiliar, introduce gradually (one service at a time).
Async chains: flatMap with async functions requires await at each step or wrapping with Promise.all.
Third-party libraries that throw: Wrap in try/catch and convert to Result at the boundary.
Too granular: Don't wrap every private helper in Result โ only public API surfaces and operations that callers need to handle explicitly.
Conventions
| Exceptions | Result Pattern |
|---|
| Error visibility | Hidden (throws anywhere) | Explicit (return type) |
| Handling | try/catch (easy to forget) | Type system forces it |
| Best for | Bugs, unexpected errors | Business errors |
Use both: exceptions for programmer errors, Result for business errors.
Resources