Skip to main content
Ganesh Joshi
Back to Blogs

Debugging Next.js in production: logs and error tracking

February 15, 20265 min read
Tips
Logs or debugging on screen

When something goes wrong in production, you need enough context to reproduce and fix it. That means structured logs, useful error messages, source maps for readable stack traces, and optionally an error-tracking service.

The debugging challenge in production

Production differs from development:

Development Production
Full error details visible Errors hidden from users
Source code available Code minified
Console always open Logs go to stdout
Reproduce easily Hard to reproduce

You need visibility into production without exposing sensitive information to users.

Structured logging

Why structure matters

Unstructured logs are hard to search:

Error: User not found
Something went wrong with payment

Structured logs are queryable:

{"level":"error","message":"User not found","userId":"123","requestId":"abc","timestamp":"2026-02-15T10:30:00Z"}
{"level":"error","message":"Payment failed","userId":"456","paymentId":"pay_789","error":"card_declined","requestId":"def"}

Building a logger

Create a simple logger wrapper:

// lib/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface LogContext {
  requestId?: string;
  userId?: string;
  [key: string]: unknown;
}

function log(level: LogLevel, message: string, context?: LogContext) {
  const entry = {
    level,
    message,
    timestamp: new Date().toISOString(),
    ...context,
  };
  
  if (process.env.NODE_ENV === 'development') {
    console.log(JSON.stringify(entry, null, 2));
  } else {
    console.log(JSON.stringify(entry));
  }
}

export const logger = {
  debug: (msg: string, ctx?: LogContext) => log('debug', msg, ctx),
  info: (msg: string, ctx?: LogContext) => log('info', msg, ctx),
  warn: (msg: string, ctx?: LogContext) => log('warn', msg, ctx),
  error: (msg: string, ctx?: LogContext) => log('error', msg, ctx),
};

Using the logger

// In Route Handlers
export async function POST(request: Request) {
  const requestId = crypto.randomUUID();
  
  try {
    const body = await request.json();
    logger.info('Processing order', { requestId, userId: body.userId });
    
    // ... process order
    
    logger.info('Order completed', { requestId, orderId: order.id });
    return Response.json({ success: true });
  } catch (error) {
    logger.error('Order failed', {
      requestId,
      error: error instanceof Error ? error.message : 'Unknown error',
    });
    return Response.json({ error: 'Order failed' }, { status: 500 });
  }
}

What to log

Do log Do not log
Request IDs Passwords
User IDs Full tokens
Error messages Credit card numbers
Relevant context PII beyond what is needed
Timestamps Full request bodies
Operation names Stack traces to users

Error boundaries in App Router

Creating error.tsx

Add error.tsx to catch errors in route segments:

// app/dashboard/error.tsx
'use client';

import { useEffect } from 'react';
import { Button } from '@/components/ui';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log to error tracking service
    console.error('Dashboard error:', error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
      <h2 className="text-xl font-semibold">Something went wrong</h2>
      <p className="text-gray-500">
        We have been notified and are working on a fix.
      </p>
      <Button onClick={reset}>Try again</Button>
    </div>
  );
}

Global error handling

For root-level errors, add global-error.tsx:

// app/global-error.tsx
'use client';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <div>
          <h2>Something went wrong</h2>
          <button onClick={reset}>Try again</button>
        </div>
      </body>
    </html>
  );
}

Error boundary hierarchy

app/
  error.tsx           # Catches app-level errors
  global-error.tsx    # Catches root layout errors
  dashboard/
    error.tsx         # Catches dashboard errors
    settings/
      error.tsx       # Catches settings errors

More specific error boundaries take precedence.

Source maps

The problem

Production code is minified:

TypeError: Cannot read property 'map' of undefined
    at r (/_next/static/chunks/pages/_app-123abc.js:1:12345)

This is not useful for debugging.

Enabling source maps

In next.config.js:

module.exports = {
  productionBrowserSourceMaps: true, // For client-side
  // Or upload to error tracking service privately
};

Private source maps

Better approach: keep source maps private and upload to your error tracking service:

// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');

module.exports = withSentryConfig(
  { /* your config */ },
  {
    hideSourceMaps: true, // Do not expose publicly
    widenClientFileUpload: true,
  }
);

Sentry (and similar tools) decode stack traces server-side.

Error tracking services

What they provide

Feature Benefit
Error aggregation See all errors in one place
Stack traces Decoded with source maps
Frequency Know which errors are common
User context See who was affected
Release tracking Know when errors started
Alerts Get notified of new errors

Setting up Sentry

npx @sentry/wizard@latest -i nextjs

This configures:

  • SDK initialization
  • Source map uploads
  • Error boundaries
  • Performance monitoring

Manual integration

// lib/sentry.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1, // 10% of transactions
  environment: process.env.NODE_ENV,
});

// Capture errors
export function captureError(error: Error, context?: Record<string, unknown>) {
  Sentry.captureException(error, { extra: context });
}

// Add user context
export function setUser(user: { id: string; email?: string }) {
  Sentry.setUser(user);
}

Request tracing

Adding request IDs

// middleware.ts
import { NextResponse } from 'next/server';

export function middleware(request: Request) {
  const requestId = crypto.randomUUID();
  const response = NextResponse.next();
  
  response.headers.set('x-request-id', requestId);
  
  return response;
}

Using request IDs in logs

export async function GET(request: Request) {
  const requestId = request.headers.get('x-request-id') || 'unknown';
  
  logger.info('Handling request', { requestId, path: '/api/users' });
  
  // ... rest of handler
}

This lets you trace a request through your logs.

Debugging checklist

Step Action
1. Find the error Check error tracking dashboard or logs
2. Get context Request ID, user ID, timestamp
3. Read stack trace Use source maps to decode
4. Check related logs Filter by request ID
5. Reproduce locally Use the same inputs
6. Fix and deploy Add tests to prevent regression

Summary

Effective production debugging requires:

  1. Structured logging with request IDs and context
  2. Error boundaries for graceful React error handling
  3. Source maps for readable stack traces (kept private)
  4. Error tracking services for centralized monitoring
  5. Request tracing to follow requests through logs

Set these up before you need them. When production breaks, you will have the context to fix it quickly.

Frequently Asked Questions

Use structured logging to stdout, error boundaries to catch React errors gracefully, source maps to decode minified stack traces, and error tracking services like Sentry for centralized error monitoring.

Log request IDs, user IDs, timestamps, error messages, and relevant context. Avoid logging sensitive data like tokens, passwords, or full request bodies.

Add an error.tsx file in route segments to catch errors and show a fallback UI. The error is logged server-side while users see a friendly error page.

Yes, but keep them private. Upload source maps to your error tracking service or host them privately. This lets you decode minified stack traces without exposing source code.

Sentry, LogRocket, and Datadog are popular choices with official Next.js integrations. They capture errors, provide stack traces, and show error frequency.

Related Posts