Apply this skill when building error handling, error boundaries, logging, monitoring, or observability in a Next.js + TypeScript + Drizzle application. Triggers on requests like "handle errors", "add error boundary", "add logging", "add monitoring", "set up Sentry", "why is this failing silently", "add a 404 page", or any time you are building features that need to handle failure gracefully. Use proactively — every new feature should account for error states.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Every operation that can fail must handle failure explicitly. Silent failures — mutations with no onError, API routes that swallow exceptions, components that render nothing on error — are bugs.
Every route segment should have its own error.tsx. One global boundary means one failure takes down the entire page.
app/(dashboard)/
├── error.tsx # Dashboard-level fallback
├── pipeline/
│ ├── page.tsx
│ └── error.tsx # Pipeline-specific error UI
├── candidates/
│ ├── page.tsx
│ └── error.tsx # Candidates-specific error UI
└── settings/
├── page.tsx
└── error.tsx # Settings-specific error UI// app/(dashboard)/pipeline/error.tsx
"use client"
export default function PipelineError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="flex flex-col items-center justify-center gap-4 p-8">
<h2 className="text-lg font-semibold">Something went wrong loading the pipeline</h2>
<p className="text-sm text-muted-foreground">
{process.env.NODE_ENV === 'development' ? error.message : 'An unexpected error occurred'}
</p>
<Button onClick={reset}>Try again</Button>
</div>
)
}global-error.tsx catches errors in the root layout itself — the last line of defence.
// app/global-error.tsx
"use client"
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold">Something went wrong</h1>
<button onClick={reset} className="mt-4 rounded bg-primary px-4 py-2 text-primary-foreground">
Try again
</button>
</div>
</body>
</html>
)
}// app/not-found.tsx
export default function NotFound() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold">404</h1>
<p className="mt-2 text-muted-foreground">Page not found</p>
<Link href="/" className="mt-4 inline-block text-primary underline">Go home</Link>
</div>
</div>
)
}Never throw to the client. Return typed results.
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string }
export async function createUser(input: unknown): Promise<ActionResult<User>> {
const parsed = UserInsertSchema.safeParse(input)
if (!parsed.success) return { success: false, error: parsed.error.issues[0].message }
try {
const [user] = await db.insert(users).values(parsed.data).returning()
return { success: true, data: user }
} catch (error) {
logger.error('createUser failed', { error, input: parsed.data })
return { success: false, error: 'Failed to create user' }
}
}Consistent error response format across all endpoints.
type ApiError = {
error: string
code: string
details?: unknown
}
function errorResponse(message: string, code: string, status: number, details?: unknown) {
return Response.json({ error: message, code, details } satisfies ApiError, { status })
}
// Usage
export async function GET(request: Request) {
const session = await getServerSession()
if (!session) return errorResponse('Unauthorised', 'UNAUTHORIZED', 401)
const params = ListParamsSchema.safeParse(Object.fromEntries(new URL(request.url).searchParams))
if (!params.success) return errorResponse('Invalid parameters', 'VALIDATION_ERROR', 400, params.error.issues)
try {
const data = await fetchData(params.data)
return Response.json(data)
} catch (error) {
logger.error('GET /api/resource failed', { error })
return errorResponse('Internal server error', 'INTERNAL_ERROR', 500)
}
}error.message or stack traces in production responsesEvery mutation must have an onError handler that tells the user something went wrong.
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userKeys.all })
toast.success('User created')
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : 'Something went wrong')
},
})const { data, isLoading, isError, error } = useQuery(userQueryOptions(id))
if (isLoading) return <Skeleton />
if (isError) return <ErrorMessage message={error.message} />
return <UserDetail user={data} />Replace console.log/console.error with structured logging.
// lib/logger.ts
type LogLevel = 'info' | 'warn' | 'error'
interface LogEntry {
level: LogLevel
message: string
timestamp: string
[key: string]: unknown
}
function log(level: LogLevel, message: string, context?: Record<string, unknown>) {
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
...context,
}
if (level === 'error') {
console.error(JSON.stringify(entry))
} else {
console.log(JSON.stringify(entry))
}
}
export const logger = {
info: (msg: string, ctx?: Record<string, unknown>) => log('info', msg, ctx),
warn: (msg: string, ctx?: Record<string, unknown>) => log('warn', msg, ctx),
error: (msg: string, ctx?: Record<string, unknown>) => log('error', msg, ctx),
}Integrate Sentry (or similar) for production error tracking.
// lib/sentry.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1, // 10% of transactions
})Not all errors are equal. Handle them differently:
| Type | Example | Response |
|---|---|---|
| Validation | Invalid form input | Show inline error, don't log |
| Auth | Expired session | Redirect to login |
| Not Found | Missing resource | Show 404 UI |
| Transient | Network timeout, DB connection | Retry with backoff, show "try again" |
| Permanent | Missing permission, deleted resource | Show error, no retry |
| Unexpected | Unhandled exception | Log + alert, show generic error |