CtrlK
BlogDocsLog inGet started
Tessl Logo

product-factory/error-handling

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.

93

Quality

93%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Skills
Evals
Files

SKILL.md

name:
error-handling
description:
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.

Error Handling & Observability: Next.js + TypeScript + Drizzle

The Core Rule

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.


Error Boundaries — Granular, Not Global

Route-Level Error Boundaries

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 Boundary

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>
  )
}

Not Found Pages

// 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>
  )
}

Server-Side Error Handling

Typed Result Pattern for Server Actions

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' }
  }
}

API Route Error Handling

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)
  }
}

Never Leak Internals

  • Never return error.message or stack traces in production responses
  • Never log full request bodies containing passwords or tokens
  • Never fail open — if auth/validation throws, deny access by default

Client-Side Error Handling

Mutation Error Handling — Never Silent

Every 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')
  },
})

Query Error States — Always Handle

const { data, isLoading, isError, error } = useQuery(userQueryOptions(id))

if (isLoading) return <Skeleton />
if (isError) return <ErrorMessage message={error.message} />
return <UserDetail user={data} />

Structured Logging

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),
}

What to Log

  • Auth events (login, logout, failed attempts)
  • Mutation failures with context (which user, which resource, what error)
  • External API call failures
  • Database query failures

What NOT to Log

  • Passwords, tokens, API keys
  • Full request bodies with PII
  • Successful reads (too noisy)

Error Monitoring

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
})

What Sentry Gives You

  • Automatic capture of unhandled exceptions (client and server)
  • Stack traces with source maps
  • User context (which user hit the error)
  • Performance monitoring (slow pages, slow API routes)
  • Alert rules (Slack/email when error rate spikes)

Error Classification

Not all errors are equal. Handle them differently:

TypeExampleResponse
ValidationInvalid form inputShow inline error, don't log
AuthExpired sessionRedirect to login
Not FoundMissing resourceShow 404 UI
TransientNetwork timeout, DB connectionRetry with backoff, show "try again"
PermanentMissing permission, deleted resourceShow error, no retry
UnexpectedUnhandled exceptionLog + alert, show generic error

Install with Tessl CLI

npx tessl i product-factory/error-handling@0.1.0

SKILL.md

tile.json