Error handling patterns: Result types, Error Boundaries, try/catch strategies, structured logging, and error monitoring setup.
Error handling patterns: Result types, Error Boundaries, try/catch strategies, structured logging, and error monitoring setup.
BeforeMerge offers hundreds of code review rules, guides, and detection patterns to help your team ship better code.
Robust error handling prevents crashes, aids debugging, and provides good user experiences.
Instead of throwing exceptions, return typed results:
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function parseEmail(input: string): Result<string> {
const email = input.trim().toLowerCase();
if (!email.includes("@")) {
return { success: false, error: new Error("Invalid email format") };
}
return { success: true, data: email };
}
// Usage — caller is forced to handle both cases
const result = parseEmail(input);
if (!result.success) {
showError(result.error.message);
return;
}
console.log(result.data); // TypeScript knows this is stringBenefits: No try/catch needed, explicit error paths, full type safety.
type ActionResult<T> = { data: T; error?: never } | { data?: never; error: string };
export async function createPost(formData: FormData): Promise<ActionResult<Post>> {
try {
const title = formData.get("title") as string;
if (!title) return { error: "Title is required" };
const post = await db.post.create({ data: { title } });
revalidatePath("/posts");
return { data: post };
} catch (e) {
console.error("createPost failed:", e);
return { error: "Failed to create post. Please try again." };
}
}Catch rendering errors and show fallback UI:
"use client";
import { Component, type ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State { hasError: boolean; error?: Error; }
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error("ErrorBoundary caught:", error, info.componentStack);
// Send to error monitoring service
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>Something went wrong.</div>;
}
return this.props.children;
}
}In Next.js App Router, use error.tsx:
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</div>
);
}// Bad — try/catch on every function
async function getUser(id: string) {
try { return await db.user.findUnique({ where: { id } }); }
catch (e) { return null; } // Swallows the error
}
// Good — let errors propagate, catch at the boundary
async function getUser(id: string) {
return await db.user.findUnique({ where: { id } });
}
// Boundary (page or API route)
try {
const user = await getUser(id);
} catch (e) {
logger.error("Failed to load user", { id, error: e });
return notFound();
}const logger = {
error(message: string, context: Record<string, unknown>) {
console.error(JSON.stringify({
level: "error",
message,
timestamp: new Date().toISOString(),
...context,
}));
},
warn(message: string, context: Record<string, unknown>) {
console.warn(JSON.stringify({ level: "warn", message, ...context }));
},
};
// Usage
logger.error("Payment failed", {
userId: user.id,
amount: 99.99,
error: err.message,
stack: err.stack,
});Integrate a service like Sentry:
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
environment: process.env.NODE_ENV,
});
// Capture errors with context
Sentry.captureException(error, {
tags: { feature: "checkout" },
extra: { userId, orderId },
});error.tsx for each route segment