Activates when working with error handling patterns across languages — Result/Either types, custom error hierarchies, error boundaries, retry/circuit breaker, structured error responses, and contextual error logging
Errors are data, not surprises. Design systems where errors are explicit, typed, and carry enough context to diagnose issues without reproducing them.
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// Usage
async function findUser(id: string): Promise<Result<User, "NOT_FOUND" | "DB_ERROR">> {
try {
const user = await db.users.findUnique({ where: { id } });
if (!user) return err("NOT_FOUND");
return ok(user);
} catch {
return err("DB_ERROR");
}
}
// Caller handles both cases explicitly
const result = await findUser(id);
if (!result.ok) {
switch (result.error) {
case "NOT_FOUND": return res.status(404).json({ error: "User not found" });
case "DB_ERROR": return res.status(500).json({ error: "Internal error" });
}
}
const user = result.value; // Type narrowed to User
Libraries: neverthrow, ts-results, effect (for advanced use cases).
// Rust has Result<T, E> built in
fn parse_config(path: &str) -> Result<Config, ConfigError> {
let content = fs::read_to_string(path)
.map_err(|e| ConfigError::IoError(path.to_string(), e))?;
let config: Config = toml::from_str(&content)
.map_err(|e| ConfigError::ParseError(e))?;
Ok(config)
}
// Go uses multiple return values
func FindUser(id string) (*User, error) {
user, err := db.QueryUser(id)
if err != nil {
return nil, fmt.Errorf("finding user %s: %w", id, err)
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
Always wrap errors with fmt.Errorf("context: %w", err) to build an error chain. Use errors.Is() and errors.As() to inspect wrapped errors.
// Base application error
abstract class AppError extends Error {
abstract readonly code: string;
abstract readonly statusCode: number;
readonly timestamp = new Date().toISOString();
constructor(message: string, readonly cause?: Error) {
super(message);
this.name = this.constructor.name;
}
toJSON() {
return {
code: this.code,
message: this.message,
timestamp: this.timestamp,
};
}
}
class NotFoundError extends AppError {
readonly code = "NOT_FOUND";
readonly statusCode = 404;
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`);
}
}
class ValidationError extends AppError {
readonly code = "VALIDATION_ERROR";
readonly statusCode = 400;
constructor(readonly fields: Record<string, string[]>) {
super("Validation failed");
}
}
class ConflictError extends AppError {
readonly code = "CONFLICT";
readonly statusCode = 409;
}
class AppError(Exception):
def __init__(self, message: str, code: str, status_code: int = 500, cause: Exception | None = None):
super().__init__(message)
self.code = code
self.status_code = status_code
self.__cause__ = cause
class NotFoundError(AppError):
def __init__(self, resource: str, resource_id: str):
super().__init__(f"{resource} {resource_id} not found", code="NOT_FOUND", status_code=404)
class ValidationError(AppError):
def __init__(self, fields: dict[str, list[str]]):
super().__init__("Validation failed", code="VALIDATION_ERROR", status_code=400)
self.fields = fields
class ErrorBoundary extends React.Component<
{ fallback: React.ComponentType<{ error: Error; reset: () => void }>; children: React.ReactNode },
{ error: Error | null }
> {
state = { error: null as Error | null };
static getDerivedStateFromError(error: Error) {
return { error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
reportError(error, { componentStack: info.componentStack });
}
render() {
if (this.state.error) {
const Fallback = this.props.fallback;
return <Fallback error={this.state.error} reset={() => this.setState({ error: null })} />;
}
return this.props.children;
}
}
// Usage — scope boundaries to feature areas, not the whole app
<ErrorBoundary fallback={OrderErrorFallback}>
<OrderDetails orderId={id} />
</ErrorBoundary>
// Express
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
if (err instanceof AppError) {
logger.warn({ err, path: req.path }, "Application error");
return res.status(err.statusCode).json(err.toJSON());
}
logger.error({ err, path: req.path }, "Unhandled error");
res.status(500).json({ code: "INTERNAL_ERROR", message: "An unexpected error occurred" });
});
// Process-level (Node.js)
process.on("unhandledRejection", (reason) => {
logger.fatal({ err: reason }, "Unhandled promise rejection");
process.exit(1); // Let process manager restart
});
async function withRetry<T>(
fn: () => Promise<T>,
opts: { maxAttempts?: number; baseDelayMs?: number; maxDelayMs?: number } = {}
): Promise<T> {
const { maxAttempts = 3, baseDelayMs = 200, maxDelayMs = 10_000 } = opts;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxAttempts) throw err;
if (!isRetryable(err)) throw err;
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs);
const jitter = delay * (0.5 + Math.random() * 0.5);
await sleep(jitter);
}
}
throw new Error("Unreachable");
}
function isRetryable(err: unknown): boolean {
if (err instanceof AppError) return false; // Business errors are not retryable
if (err instanceof Error && "status" in err) {
const status = (err as any).status;
return status === 429 || status >= 500;
}
return true;
}
Prevent cascading failures by stopping calls to a failing downstream service.
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: "closed" | "open" | "half-open" = "closed";
constructor(
private readonly threshold: number = 5,
private readonly resetTimeoutMs: number = 30_000
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === "open") {
if (Date.now() - this.lastFailure > this.resetTimeoutMs) {
this.state = "half-open";
} else {
throw new CircuitOpenError("Circuit is open");
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private onSuccess() {
this.failures = 0;
this.state = "closed";
}
private onFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = "open";
}
}
}
Always return a consistent error shape from APIs.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be at least 18" }
],
"requestId": "req_abc123",
"timestamp": "2026-01-15T10:30:00Z"
}
}
Include a requestId for correlation. Never expose stack traces, internal paths, or database details in production responses.
// Good — structured with context
logger.error({
err,
userId: req.user?.id,
orderId: cmd.orderId,
action: "confirm_order",
duration: Date.now() - start,
}, "Failed to confirm order");
// Bad — unstructured, no context
console.error("Error: " + err.message);
Fail fast when:
Degrade gracefully when:
catch (e) {} hides bugs and makes debugging impossiblethrow "something went wrong" loses stack traces and type information{ success: false } without error details forces callers to guess