Error Handling Patterns in TypeScript/React
Robust error handling prevents crashes, aids debugging, and provides good user experiences.
Result Types
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 string
Benefits: No try/catch needed, explicit error paths, full type safety.
Server Action Pattern
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." };
}
}
React Error Boundaries
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 >
);
}
Try/Catch Strategies
Catch at boundaries, not everywhere
// 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 ();
}
Structured Logging
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,
});
Error Monitoring Setup
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 Handling Checklist