SDK & Framework Guides

Error Tracking for Server-Side Rendered

Server-side rendering error tracking for SSR apps. Unified monitoring for Next.js, Remix, Nuxt—catch hydration errors, server failures, and client crashes.

Server-side rendering (SSR) unlocks faster initial page loads and better SEO, but it introduces a new class of errors that purely client-side apps don't face. When you render React, Vue, or Svelte on the server and then hydrate on the client, failures can happen in either context—or in the gap between them. Server-side rendering error tracking SSR monitoring means catching bootstrap failures, hydration mismatches, and client-side crashes in one unified dashboard, with source maps working in both contexts.

Traditional error tracking was built for client-side apps. Server errors are often logged to files or stdout; client-side errors land in a separate tool. In an SSR app, this split is a liability. A hydration mismatch might silently tear down your component tree on the client, leaving the user with a broken page—and you'll never know unless you're watching both logs and frontend dashboards at once. Server-side rendering error tracking solves this by capturing errors at the point they occur, whether that's a database query failing during server render or a React hook crashing during client hydration.

Why SSR Error Tracking Requires a Different Approach

SSR frameworks like Next.js, Remix, and Nuxt.js blur the line between server and client code. A single file might have functions that run on the server and methods that run in the browser. A thrown error in a server component or API route needs different handling than a runtime error in client JavaScript.

The complexity grows when you factor in the hydration step. During hydration, React (or Vue, or Svelte) reconstructs your component tree on the client by running the same code that rendered on the server. If the server and client render different content—perhaps because of a time-dependent value, a random number, or async data handled differently—React can't match the DOM and silently replaces large parts of the tree. You won't see an error message. Users just see a flash of unstyled content, missing components, or worse, entirely broken interactions.

Other challenges unique to SSR:

  • Async data races: Data fetched on the server might not be ready when the client initializes.
  • Environment variable mismatches: Server code reads from .env.local; client code shouldn't, but sometimes does.
  • Module resolution differences: Node.js and browser module resolution behave differently; a package that works on server might fail on client.
  • Timing-dependent bugs: Layout shifts, event listeners attaching before hydration completes, or third-party scripts interfering.

Categorizing SSR Errors by When They Occur

To track errors effectively, you need to label them by lifecycle stage. LightTrace's error tracking features work across all stages of an SSR app.

Server bootstrap errors happen when your server starts. A missing environment variable, database connection failure, or syntax error in a server module can prevent the app from launching. These should trigger immediate alerts—your entire server is down.

Render-time server errors occur while generating HTML. A database query fails, a third-party API is unreachable, or a server component throws an exception. The error context includes request headers, cookies, user ID, and other request-scoped data.

Hydration errors surface on the client once the page loads. React, Vue, or Svelte detects that the server-rendered HTML doesn't match the client-rendered tree. The framework logs a warning (sometimes an error). Hydration mismatches are common and silent—you need instrumentation to catch them.

Post-hydration runtime errors are ordinary client-side crashes after hydration completes. These look like React errors you'd see in any client app, but now they might be interacting with server state.

Each category needs different instrumentation. Your error tracking setup must emit events for all of them and attach context so you can reproduce the issue.

Setting Up Error Tracking for Your SSR Framework

Most SSR frameworks work with the Sentry SDK. Since LightTrace speaks the Sentry protocol, you can use your existing Sentry SDK—just change the DSN to point at LightTrace.

Here's a minimal Next.js setup with @sentry/nextjs:

// instrumentation.ts or pages/_app.tsx
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: "https://<key>@light-trace.robomiri.com/1",
  environment: process.env.NODE_ENV,
  tracesSampleRate: 0.1,
  integrations: [
    new Sentry.Replay({
      maskAllText: true,
      blockAllMedia: true,
    }),
  ],
});

// Catch unhandled server errors
process.on("unhandledRejection", (reason) => {
  Sentry.captureException(reason);
});

For Remix, initialize in your entry.server.tsx and entry.client.tsx:

// entry.server.tsx
import * as Sentry from "@sentry/remix";

Sentry.init({
  dsn: "https://<key>@light-trace.robomiri.com/1",
  environment: process.env.NODE_ENV,
});

// In your loader or action:
export async function loader({ request }) {
  try {
    const data = await fetchData();
    return json(data);
  } catch (error) {
    Sentry.captureException(error);
    throw new Response("Server error", { status: 500 });
  }
}

For Nuxt 3, add the module to nuxt.config.ts:

export default defineNuxtConfig({
  modules: ["@sentry/nuxt/module"],
  sentry: {
    dsn: "https://<key>@light-trace.robomiri.com/1",
    environment: process.env.NODE_ENV,
  },
});

In all cases, configure the SDK to capture errors on the server and the browser. The key is setting the DSN once and letting the SDK detect whether it's running in Node or the browser.

Handling Hydration Mismatches

Hydration errors are notoriously hard to debug because the framework usually doesn't throw; it just logs a warning. To make them visible in your error tracking, you need to hook into the framework's error boundary or error handler.

In Next.js with the App Router, use an error boundary component:

// app/error.tsx
"use client";

import { useEffect } from "react";
import * as Sentry from "@sentry/nextjs";

export default function Error({ error, reset }) {
  useEffect(() => {
    Sentry.captureException(error);
  }, [error]);

  return (
    <div>
      <h1>Something went wrong</h1>
      <button onClick={() => reset()}>Retry</button>
    </div>
  );
}

In Remix, catch errors in your root loader and action:

export function ErrorBoundary({ error }) {
  const isRouteError = isRouteErrorResponse(error);

  useEffect(() => {
    if (!isRouteError) {
      Sentry.captureException(error);
    }
  }, [error, isRouteError]);

  return (
    <html>
      <body>{/* ... */}</body>
    </html>
  );
}

For Vue/Nuxt, add a global error handler:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
    Sentry.captureException(error, {
      contexts: {
        vue: {
          componentName: instance.$options.name,
          lifecycleHook: info,
        },
      },
    });
  };
});

These handlers won't catch pure hydration mismatches (React doesn't throw; it just replaces the tree), but they'll catch component initialization errors that occur during hydration. For detection of silent mismatches, add a client-side hydration check:

// utils/hydration.ts
export function detectHydrationMismatch() {
  if (typeof document === "undefined") return;

  // Simple check: verify the initial render matches
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === "childList") {
        const hasLargeChange = mutation.addedNodes.length > 5;
        if (hasLargeChange) {
          Sentry.captureMessage("Possible hydration mismatch detected", {
            level: "warning",
            contexts: {
              hydration: {
                mutationCount: mutation.addedNodes.length,
                mutationType: mutation.type,
              },
            },
          });
          observer.disconnect();
        }
      }
    });
  });

  observer.observe(document.body, { childList: true, subtree: true });

  // Stop after 2 seconds (hydration should be complete)
  setTimeout(() => observer.disconnect(), 2000);
}

Run your hydration check only in development or under a feature flag in production—large DOM changes can be legitimate if your app uses code splitting or lazy loading.

Source Maps for Server and Client Code

Source maps are essential for debugging. Without them, you'll see minified variable names and line numbers that don't correspond to your source code. In an SSR app, you need source maps for both the server bundle and the client bundle.

Most SSR frameworks generate source maps by default in development and can be configured to include them in production (with warnings about bundle size). For LightTrace, upload source maps during your build or CI/CD step so that stack traces are automatically de-minified.

In Next.js, ensure next.config.js has source maps enabled:

module.exports = {
  productionBrowserSourceMaps: true,
  typescript: {
    ignoreInternalErrors: true,
  },
};

For Remix, configure in remix.config.js:

module.exports = {
  publicPath: "/build/",
  sourcemap: true,
};

Then, integrate source map upload into your deploy pipeline. Most CI/CD systems support uploading after a build completes. Consult your framework's docs and error tracking guides for framework-specific source map handling.

Unified Monitoring: Connect Your Entire Request Flow

In an SSR app, a single user request can trigger errors in both server and client. Your error tracking tool should link them together so you can see the full picture.

Use distributed tracing to connect server and client spans. When your server renders a page, start a trace and pass the trace ID to the client (often in a <script> tag or header). On the client, attach the same trace ID to any subsequent spans.

In Next.js:

// pages/api/trace.ts
import * as Sentry from "@sentry/nextjs";

export default function handler(req, res) {
  const transaction = Sentry.startTransaction({
    name: "GET /api/data",
    op: "http.server",
  });

  // Perform work...

  transaction.finish();

  // Pass trace ID to client
  res.setHeader("X-Trace-ID", transaction.traceId);
  res.json({ traceId: transaction.traceId });
}

Then on the client, attach it to subsequent requests:

// Client code
const traceId = document.querySelector("meta[name='trace-id']")?.content;

fetch("/api/data", {
  headers: { "X-Trace-ID": traceId },
}).then((res) => res.json());

LightTrace's distributed tracing features let you follow a request across service boundaries. For SSR, this means seeing when a client request spawns a server fetch, which might trigger a database query. One error anywhere in the chain surfaces in one place.

Distributed tracing is especially useful in SSR when your server fetches data during render. If a user's request times out waiting for server-side data fetching, the trace will show you exactly where the bottleneck is.

Debugging Production SSR Errors

When an error surfaces in production, your first step is to examine the full context. LightTrace captures breadcrumbs (a log of user actions and system events leading up to the error), request headers, user information, and custom tags you add.

For SSR-specific debugging, add context about the render phase:

Sentry.addBreadcrumb({
  message: "Starting server render",
  level: "info",
  category: "ssr",
  data: {
    url: req.url,
    method: req.method,
  },
});

try {
  const html = await renderToString(app);
} catch (error) {
  Sentry.captureException(error, {
    tags: {
      phase: "server-render",
      framework: "nextjs",
    },
    contexts: {
      server: {
        url: req.url,
        userAgent: req.headers["user-agent"],
      },
    },
  });
}

When you see an error in your LightTrace dashboard, click into it. You'll see the full stack trace (de-minified with source maps), all breadcrumbs, affected user info, and release information. If how to debug production errors is part of your workflow, you can link directly to the file and line on GitHub (if you've connected your repository) and jump straight to the code.

Common SSR Pitfalls and How to Avoid Them

  1. Forgetting to serialize state: If you hydrate with state that doesn't match the server-rendered HTML, the client will mismatch. Ensure your data serialization is deterministic and identical on both server and client.

  2. Using browser-only APIs during server render: Code like window.localStorage or document.querySelector will crash on the server. Check typeof window !== "undefined" before using them.

  3. Mismatched dependency versions: Sometimes a dependency's server-side behavior differs from its browser behavior. Pin versions strictly.

  4. Forgetting to await async initialization: If your app needs async setup (loading config, connecting to databases), ensure the server waits for it before rendering. Delayed initialization on the client can cause mismatches.

Error tracking won't prevent these issues, but it will make them visible immediately and let you pinpoint exactly where they're happening.


Server-side rendering error tracking SSR is non-negotiable for SSR apps. It bridges the gap between server and client, catches silent failures like hydration mismatches, and gives you the full context you need to debug fast. Set up error tracking early, configure it for both server and client, and lean on source maps to make stack traces readable.

Start tracking errors in minutes

Start monitoring your SSR app for free with LightTrace. No credit card, no setup hassle—just point your Sentry SDK at our endpoint and get unified error tracking across your entire stack. Try LightTrace free today.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.