Error Handling
Error handling with Sentry integration and app-level error catalogs.
Complete guide for error handling across the monorepo: @repo/sentry for reporting to Sentry, @repo/utils/error for message extraction, and app-level catalogs for safe API responses.
Overview
@repo/sentry–captureError(options)sends errors to Sentry (void; does not return a catalog). Use platform subpaths:node,nextjs,browser,react.@repo/utils/error–getErrorMessage(error)and related utilities for type-safe message extraction.- Sentry initialization – Not in
@repo/sentry. Initialize per your platform’s Sentry docs (Node, Next.js, Browser). - Error catalogs and safe responses – Implemented in each app (e.g. Fastify’s
apps/fastify/src/lib/catalogs/withgetError,mapHttpStatusToErrorCode). Apps build the safe{ code, message }response themselves.
Key principles:
- Two-track: Sentry gets real errors (debugging); API returns safe catalog errors (users), built by the app.
- Security-first: Sentry built-in PII scrubbing; never expose internals to users.
- Performance: Async Sentry capture, non-blocking.
Quick Start
Capture Error (Most Common)
// For Next.js apps
import { captureError } from '@repo/sentry/nextjs'
// For Node.js/Fastify apps
// import { captureError } from '@repo/sentry/node'
captureError({
error,
label: 'API Call',
code: 'NETWORK_ERROR', // optional, for Sentry tags
data: { endpoint: '/api/data' },
tags: { app: 'web' },
})
// void – does not return a catalog. Build API response from your app's catalog (e.g. getError).Extract Error Message
import { getErrorMessage } from '@repo/utils/error'
const message = getErrorMessage(error) // Type-safeInitialize Sentry
Sentry is not initialized by @repo/sentry. Follow your platform’s setup:
- Next.js: Sentry Next.js guide –
withSentryConfig,instrumentation.ts, etc. - Node/Fastify: Sentry Node guide
- Browser: Sentry Browser guide
Core Concepts
Two-Track Error Handling
Critical Distinction: When an error occurs, two separate things happen:
- Sentry (Internal) - REAL error with full stack trace and internal context for debugging
- API Response (External) - SAFE catalog error with user-friendly message
User Action → Error Occurs
↓
├─→ [Sentry] Real error + stack trace + internal context (developers)
└─→ [API Response] Safe catalog error { code, message } (users)Example (Fastify):
// Real error occurs (e.g., database connection failure)
const realError = new Error('Connection to postgres://internal-db:5432 failed: ECONNREFUSED')
// captureError sends REAL error to Sentry (void)
captureError({
error: realError,
label: 'Database Connection',
data: { host: 'internal-db', port: 5432 },
tags: { app: 'api' },
})
// App returns SAFE catalog error from its own catalog (e.g. getError('SERVER_ERROR'))
const catalogError = getError('SERVER_ERROR') ?? { code: 'UNEXPECTED_ERROR', message: 'An unexpected error occurred' }
reply.status(500).send(catalogError)
// ❌ User NEVER sees: stack traces, connection strings, internal IPsWhy:
- Security: Users never see sensitive internal details
- UX: Users see friendly, actionable messages
- Debugging: Developers have full context in Sentry
App-level error catalogs
Error catalogs and safe responses are implemented per app, not in @repo/sentry. For example, the Fastify app defines catalogs in apps/fastify/src/lib/catalogs/ (e.g. server.ts, client.ts, common.ts) and exposes getError(code), mapHttpStatusToErrorCode(statusCode). The error handler calls captureError from @repo/sentry/node then builds the response using the app’s getError.
Adding new error codes: Add them to your app’s catalog (e.g. apps/fastify/src/lib/catalogs/), then use getError(yourCode) when building responses. Use UPPER_SNAKE_CASE (e.g. NETWORK_ERROR, USER_NOT_FOUND).
Security
Sentry's Built-in PII Scrubbing: Sentry automatically handles sensitive data scrubbing. No custom beforeSend hook required by default.
Why Sentry's built-in approach:
- Battle-tested and maintained by Sentry
- Handles edge cases (circular refs, depth limits, etc.)
- No custom code to maintain
- Works across all event types (errors, breadcrumbs, contexts)
- Automatically scrubs: password, token, secret, apiKey, authorization, cookie, session
Optional Custom Scrubbing: Add a beforeSend hook to Sentry initialization only if you need domain-specific scrubbing beyond Sentry's defaults.
Never expose internal details to users - Always return safe catalog errors.
Framework Integration
Fastify (Backend API)
Global Error Handler Plugin
Use captureError from @repo/sentry/node (void); build the response from the app’s catalog (getError, mapHttpStatusToErrorCode from apps/fastify/src/lib/catalogs/):
// apps/fastify/src/plugins/error-handler.ts
import { captureError } from '@repo/sentry/node'
import { getError, mapHttpStatusToErrorCode } from '../lib/catalogs/mapper.js'
// ... route extraction, redaction ...
fastify.setErrorHandler((error, request, reply) => {
const statusCode = error.statusCode ?? 500
const errorCode = mapHttpStatusToErrorCode(statusCode)
captureError({
error,
logger: request.log,
label: `${request.method} ${request.url}`,
data: { method: request.method, url: request.url, headers: sanitizedHeaders, body: sanitizedBody },
tags: { app: 'api', module, route: routePath, method: request.method },
})
const catalogError = getError(errorCode) ?? getError('UNEXPECTED_ERROR') ?? { code: 'UNEXPECTED_ERROR', message: 'An unexpected error occurred' }
reply.status(statusCode).send({ code: catalogError.code, message: catalogError.message })
})Setup: Initialize Sentry with @sentry/node at app bootstrap (see Sentry Node). Then register the error handler plugin. Routes can throw; the global handler captures to Sentry and returns the app’s catalog response.
React (Frontend Components)
Error Boundaries
Recommended: Use AppErrorBoundary
// apps/next/app/layout.tsx
'use client'
import { AppErrorBoundary } from '@repo/sentry/react'
import { captureError } from '@repo/sentry/nextjs'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<AppErrorBoundary app="web" captureError={captureError}>
{children}
</AppErrorBoundary>
)
}Custom implementation: Use captureError from @repo/sentry/nextjs in onError; it returns void. Show user-facing message via your fallback (e.g. getErrorMessage(error) from @repo/utils/error).
Event Handlers
import { captureError } from '@repo/sentry/nextjs'
import { getErrorMessage } from '@repo/utils/error'
import { toast } from 'sonner'
async function handleSubmit() {
try {
await submitForm()
} catch (error) {
captureError({ error, label: 'Form Submission', tags: { app: 'web', module: 'form-handler' } })
toast.error(getErrorMessage(error)) // or use your app's catalog for a safe message
}
}Use for: App-level, section-level, feature-level error handling, third-party component errors
Don't use for: Event handlers (use try/catch with captureError), async code (use try/catch), SSR errors
Next.js (App Router)
Error Pages
// app/error.tsx
'use client'
import { captureError } from '@repo/sentry/nextjs'
import { useEffect } from 'react'
export default function ErrorPage({
error,
reset
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
captureError({
code: 'UNEXPECTED_ERROR',
error,
label: 'Next.js Error Page',
tags: { app: 'web', module: 'error-page' },
data: {
digest: error.digest,
},
})
}, [error])
return (
<div>
<h2>Something went wrong</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}Server Actions
Pattern 1: Return result object – Capture with captureError, then return a safe error (e.g. from your app’s catalog or getErrorMessage):
'use server'
import { captureError } from '@repo/sentry/nextjs'
import { getErrorMessage } from '@repo/utils/error'
export async function serverAction() {
try {
const result = await db.query('...')
return { success: true, data: result }
} catch (error) {
captureError({ error, label: 'Server Action', tags: { app: 'web', module: 'server-action' } })
return { success: false, error: { message: getErrorMessage(error) } }
}
}Pattern 2: Throw error – Capture then throw; Next.js handles serialization.
'use server'
import { captureError } from '@repo/sentry/nextjs'
export async function serverAction() {
try {
return await db.query('...')
} catch (error) {
captureError({ error, label: 'Server Action', tags: { app: 'web', module: 'server-action' } })
throw error
}
}Initialization: Sentry is not initialized by @repo/sentry. Use Sentry’s Next.js setup in next.config, instrumentation.ts, etc.
API Reference
captureError(options): void (@repo/sentry/*)
Sends the error to Sentry asynchronously. Does not return a catalog; each app builds its own response.
import { captureError } from '@repo/sentry/nextjs' // or /node, /browser
captureError({
error: unknown, // Converted to Error if needed; full stack sent to Sentry
label: string, // Component/feature label
code?: string, // Optional tag for Sentry
data?: Record<string, unknown>,
tags?: Record<string, string>,
level?: 'error' | 'warning' | 'info',
report?: boolean, // If false, skip Sentry (default true)
logger?: Logger, // Optional; in Fastify pass request.log
})See packages/sentry/README.md for full options. Serverless: Consider Sentry.flush() before function exit if the process terminates quickly.
getErrorMessage(error) (@repo/utils/error)
import { getErrorMessage } from '@repo/utils/error'
const message = getErrorMessage(error) // Type-safe extraction from unknownApp-level: getError, mapHttpStatusToErrorCode
These live in each app (e.g. Fastify’s apps/fastify/src/lib/catalogs/mapper.ts). mapHttpStatusToErrorCode(statusCode) maps HTTP status to a catalog code; getError(code) returns { code, message } for the response. Not part of @repo/sentry.
Best Practices
Common Patterns
import { getErrorMessage } from '@repo/utils/error'
import { captureError } from '@repo/sentry/nextjs'
// Extract message
const message = getErrorMessage(error)
// Capture with context (void)
captureError({ error, label: 'Operation Name', tags: { app: 'api' } })
// In route/handler: capture then return safe response from app catalog
captureError({ error, label: 'Validation', tags: { app: 'api' } })
return reply.status(422).send(getError('INVALID_INPUT'))Anti-Patterns
// ❌ DON'T: Silent failures, expose internals, type assertions, console.*
catch (error) { /* silent */ }
return { error: error.message, stack: error.stack }
const message = (error as Error).message
// ✅ DO: Capture, return safe messages, use getErrorMessage
captureError({ error, label: 'Op', tags: { app: 'api' } })
return { message: getErrorMessage(error) } // or getError(code) from app catalogDecision Tree
Error occurs
│
├─ React component render? → Use Error Boundary (automatic)
├─ Fastify route? → Let error handler catch (automatic)
├─ Next.js Server Action? → Capture and return error object or throw
├─ Event handler (onClick, onSubmit)? → Try/catch with captureError + feedback
├─ Async code (Promise)? → Try/catch with captureError
├─ Background job/worker? → Capture with captureError, log for monitoring
├─ API route handler? → Let Fastify error handler catch (automatic)
└─ Recoverable?
├─ Yes → Capture with 'warning' + retry/fallback
└─ No → Capture with 'error' + show user message