Setting up error tracking in Next.js 15 without losing your mind

App Router, Server Actions, edge runtime, middleware — every place errors hide in modern Next.js, and how to capture them all.

GlitchReplay team··
nextjstutorial

You've just migrated to Next.js 15. The App Router is humming, your Server Actions are sleek, and your Middleware is handling auth perfectly—until a user reports a "white screen of death" that doesn't appear in your logs. In Next.js 15, an error in a Server Action behaves differently than an error in a Client Component, and an Edge Runtime crash might never even hit your traditional logging pipeline. The "it just works" promise of error tracking often breaks down in the multi-runtime reality of modern Vercel or Cloudflare deployments. It's time to stop guessing and start capturing.

The Four-Headed Beast: Why Next.js 15 is harder to monitor

Modern Next.js isn't a monolith; it's a distributed system running in your browser, on a Node.js server, at the edge, and during the build-time phase. Each of these environments has its own constraints, its own global objects, and its own way of failing. When you use a traditional logging library designed for a simple Express server, you're only seeing about 25% of the picture.

The fragmentation of Client, Server, Edge, and Middleware

In Next.js 15, the execution context shifts constantly. A single user request might trigger Middleware (Edge Runtime), then a Server Component (Node.js or Edge), which then hydrates into a Client Component (Browser). If the Middleware fails because of a malformed cookie, it crashes before the App Router even initializes. If a Server Component fails during streaming, the browser might receive a partial HTML document and a cryptic error in the console.

The build-time environment is the fourth head. If your generateStaticParams fails during a production build because an upstream API is down, you want that error in your dashboard, not just buried in a CI/CD log that nobody looks at until the deploy fails. Traditional console.error calls are ephemeral; they vanish as soon as the lambda execution ends or the browser tab is closed.

Why console.error is where data goes to die in production

Relying on stdout for error tracking in a serverless environment is a recipe for blind spots. When an error occurs in a Vercel Function or a Cloudflare Worker, the logs are often truncated or aggregated in ways that strip the stack trace. More importantly, console.error provides zero context. It doesn't tell you what the user was doing, what their browser version was, or the state of their Redux/Zustand store. You get the "what" but never the "why."

Leveraging instrumentation.ts for Unified Tracking

Next.js 15 has stabilized the instrumentation.ts (or .js) file, which is now the recommended way to integrate observability tools. This file runs once in each runtime (Node.js and Edge) when the server starts up, making it the perfect place to initialize your SDK without bloating your client-side bundle or duplicating logic in every route.

Configuring next.config.js for the instrumentation hook

While the hook is stable in Next.js 15, you still need to ensure your configuration is prepared to handle the dual-runtime nature of the App Router. The SDK you choose must be able to detect whether it's running in a Node.js environment or the restricted Edge Runtime.

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    // Though stable in 15, some environments still prefer explicit enablement
    instrumentationHook: true,
  },
};

module.exports = nextConfig;

The requestContext – Why global variables fail in async environments

One of the biggest traps in Next.js error tracking is state leakage. Because Next.js uses AsyncLocalStorage under the hood to manage headers and cookies, you cannot rely on global variables to store user context for your errors. Your instrumentation.ts needs to be robust enough to hook into the lifecycle of the request without interfering with the server's ability to handle concurrent users.

A robust instrumentation.ts implementation looks like this:

export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { init } = await import('@glitchreplay/nextjs');
    init({
      dsn: process.env.NEXT_PUBLIC_GLITCHREPLAY_DSN,
      tracesSampleRate: 1.0,
      environment: process.env.NODE_ENV,
    });
  }

  if (process.env.NEXT_RUNTIME === 'edge') {
    const { init } = await import('@glitchreplay/nextjs-edge');
    init({
      dsn: process.env.NEXT_PUBLIC_GLITCHREPLAY_DSN,
    });
  }
}

Deep Dive: Capturing Server Action Failures

Server Actions are effectively "hidden" POST endpoints generated by Next.js. They are incredibly convenient for form submissions and data mutations, but they are a "black box" for monitoring. When a Server Action throws an error, Next.js catches it and returns a generic 500 error to the client to prevent sensitive data leakage. This is great for security, but terrible for debugging.

Try/Catch patterns that don't swallow stack traces

If you wrap your Server Action in a basic try/catch and return an error message to the UI, you've just "handled" the error in the eyes of most SDKs. It won't be reported because it didn't crash the process. You need to manually report these "soft" failures while still providing a good UX.

'use server';

import { captureException } from '@glitchreplay/nextjs';

export async function updateProfile(formData: FormData) {
  try {
    const data = await db.user.update({ ... });
    return { success: true, data };
  } catch (error) {
    // Capture the full error before returning a safe version to the UI
    captureException(error, {
      extra: { action: 'updateProfile' }
    });
    
    return { success: false, message: 'Something went wrong' };
  }
}

Linking Action errors to the specific user session

An error in a Server Action is often the result of a specific user state. By using the captureException call within the action, you can attach tags like user_id or org_id using the next/headers API. This allows you to jump from a server-side crash directly to the user's session replay to see what they were doing leading up to the failure.

The Middleware & Edge Runtime Trap

Middleware is the first line of defense in a Next.js app. It runs in the Edge Runtime, which is a stripped-down environment based on V8. It doesn't have access to Node.js APIs like fs or net. If your error tracking SDK is bulky or relies on Node-specific internals, it will crash your Middleware or significantly increase your latency.

Handling "Lightweight" runtime limitations

Vercel and Cloudflare impose strict execution limits on Middleware—often as low as 50ms. If your SDK takes 30ms to initialize and 40ms to send an error report over HTTP, you've already exceeded your budget. This is why using a fetch-based, lightweight SDK is non-negotiable for Next.js 15. You cannot afford the "Sentry Tax" of a 100kb polyfilled bundle in your Edge functions.

Why your SDK must be compatible with the Fetch API

In the Edge Runtime, there is no XMLHttpRequest. Every piece of data sent to your monitoring service must use the standard Web fetch API. Furthermore, because Edge functions are often killed immediately after a response is returned, your SDK must support event.waitUntil() to ensure the error report is actually sent before the runtime shuts down.

Client-Side: Beyond the error.tsx Boundary

Next.js 15 uses error.tsx files as React Error Boundaries. When a component in a subtree crashes, the nearest error.tsx is rendered. This is perfect for the UI, but it's a passive mechanism. It doesn't "tell" you that an error happened; it just hides the mess from the user.

Manual reporting in useEffect within Error Boundaries

To turn error.tsx into a monitoring tool, you must explicitly trigger a report within the component. Since error.tsx must be a Client Component, you have access to browser globals and session context.

'use client';

import { useEffect } from 'react';
import { captureException } from '@glitchreplay/nextjs';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log the error to GlitchReplay
    captureException(error);
  }, [error]);

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Capturing unhandled promise rejections and "ResizeObserver" noise

Not every client-side error happens inside a React render cycle. Async clicks, setTimeout callbacks, and unhandled promise rejections are common culprits. A good setup will listen for unhandledrejection events globally. However, you also need to filter out "noise"—errors from browser extensions or benign issues like ResizeObserver loop limit exceeded that don't actually affect user experience but eat up your event quota.

Making Sense of the Noise: Source Maps and Symbols

If you've ever looked at a production stack trace and seen at a (chunks/724-abc123.js:1:450), you know the frustration of minified code. In Next.js, where code is split into dozens of small chunks, source maps are the only way to map that error back to /lib/auth.ts:42.

Uploading maps during the next build process

You should never serve source maps to your users—it's a security risk. Instead, your build pipeline should upload these maps directly to your tracking provider. Many providers charge for this storage or make the integration difficult. In GlitchReplay, this is handled via a simple CLI command that runs after next build, ensuring that every crash is automatically de-minified.

# Example deployment script
next build
glitchreplay-cli upload-sourcemaps .next/static/chunks --release=$GIT_COMMIT

The hidden cost of "per-event" pricing

When you start capturing source maps and detailed stack traces, the amount of data sent to your provider increases. If you are on a per-event pricing model, a single "hot loop" bug can result in a $5,000 bill overnight. This "success tax" is why many teams end up disabling error tracking for their high-traffic routes—exactly where they need it most.

Session Replay: The "Video" Evidence for React State

Knowing that TypeError: cannot read property 'id' of undefined occurred is useful. Knowing that it happened because a user double-clicked a "Submit" button while a Suspense boundary was flickering is transformative. Session Replay is the "missing link" in Next.js 15 observability.

Because the App Router relies heavily on complex state transitions—Parallel Routes, Intercepting Routes, and optimistic updates—the actual "bug" is often not in the code that crashed, but in the sequence of actions that led to an invalid state. Replay allows you to watch the DOM as it changed, see the network requests as they fired, and inspect the console logs in sync with the video. This reduces "Time to Resolution" from hours of guesswork to minutes of observation.

Privacy by design: Masking PII in session recordings

Recording user sessions sounds scary for compliance. However, modern replay tools default to "mask all" mode. Instead of recording actual text, the SDK records the structure of the DOM and fills in the blanks with placeholders. You see that a user typed into the "Credit Card" field, but you never see the numbers. This allows you to debug Next.js 15 apps in highly regulated industries like FinTech or Healthcare without compromising PII.

Conclusion: Flattening the Stack (and the Bill)

Next.js 15 represents a peak in web complexity. We are no longer just building "websites"; we are building distributed React applications that span multiple runtimes. Monitoring this complexity requires a tool that understands the App Router, handles the Edge Runtime without latency penalties, and doesn't penalize you for wanting full visibility.

By moving away from legacy providers with archaic per-event pricing and towards a unified, Sentry-compatible approach built on Cloudflare, you can finally capture 100% of your errors. You don't need to choose which pages to monitor based on your budget. You don't need to sacrifice stack trace depth to save on storage. With a properly configured instrumentation.ts and Session Replay, you can stop debugging logs and start building features.

The Checklist for a "Zero-Blind-Spot" Next.js Setup

  • Enable instrumentation.ts for both Node.js and Edge runtimes.
  • Wrap Server Actions in a utility that captures exceptions before re-throwing.
  • Manually report errors in your error.tsx client components.
  • Automate source map uploads in your CI/CD pipeline.
  • Turn on Session Replay with strict PII masking to see the context behind the crash.

Stop paying a "success tax" on your Next.js 15 errors—get full App Router visibility, Session Replay, and 100% Sentry-SDK compatibility with GlitchReplay's flat-rate pricing.

Stop watching your error bill spike.

GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.