Remix gives you full control over error handling with its handleError export, but control without visibility is silent failure in production. A Remix error tracking setup guide should bridge the gap between your local error boundaries and what's actually breaking for users. This post walks you through integrating LightTrace—a Sentry-compatible error tracking service—into your Remix app, so you catch server crashes, client-side exceptions, and routing errors without building custom logging infrastructure.
The key insight: Remix separates server and client errors deliberately. Your handleError function runs on the server, but client-side errors never reach it. You need both layers instrumented. Unlike manual error handling that relies on you catching and logging every exception, a proper error tracking service passively captures unhandled exceptions, preserves full stack traces, and groups them by root cause—giving you signal instead of noise.
Why Remix apps need centralized error tracking
Remix's app/root.jsx can export an ErrorBoundary, and you can define one in any route. These catch render errors and let you show a fallback UI. But error boundaries don't catch:
- Server-side exceptions in loaders and actions
- Network errors during data fetching
- Exceptions in middleware or utility functions
- Client-side JavaScript errors outside React
Logging these manually is error-prone. A developer adds a try-catch in a loader, logs to console, and forgets to add the same instrumentation in an action. The error happens once a month in production—you never see it.
Error tracking services solve this by instrumenting your entire runtime: server errors are captured from handleError, client errors are captured by a global handler, and both are grouped by fingerprint. When the same bug hits again, you're notified of a recurrence, not a new issue.
Getting your LightTrace DSN
LightTrace uses the Sentry protocol, so any Sentry SDK works as-is. You just change the DSN.
- Sign up at LightTrace (or log in if you already have an account).
- Create a project—select "Remix" as the platform, or choose "Node.js" for the server and "Browser" for the client (you'll get two separate DSNs, one per runtime).
- Copy your DSN—it looks like
https://<key>@light-trace.robomiri.com/1.
For a single Remix app, one DSN often works for both server and client events—LightTrace sorts them by runtime automatically. If you want strict separation, use two projects and two DSNs.
Installing and configuring the Sentry SDK
Add the official Sentry SDK for JavaScript:
npm install @sentry/remix @sentry/tracing
In your app/root.jsx or app/entry.client.jsx, initialize Sentry on the client:
// app/entry.client.jsx
import * as Sentry from "@sentry/remix";
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
tracesSampleRate: 1.0, // Capture all transactions in development; lower in production
environment: process.env.NODE_ENV,
});
export const HydrateFallback = () => <div>Loading...</div>;
On the server side, create app/instrumentation.server.js:
// app/instrumentation.server.js
import * as Sentry from "@sentry/remix";
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV,
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
],
});
Then import it at the top of your server.js or entry point, before any routes load:
// server.js
import "./app/instrumentation.server.js";
// ... rest of your server setup
If you run Remix in a non-standard environment (e.g., on Vercel or Cloudflare), check the add-error-tracking-to-your-app guide for runtime-specific setup notes.
Wiring handleError to Sentry
Remix's handleError export lets you observe every uncaught server error. Wire it into Sentry:
// app/root.jsx
import * as Sentry from "@sentry/remix";
export const handleError = async (error, { request }) => {
// Capture the error in Sentry, attaching the request context
Sentry.captureException(error, {
contexts: {
http: {
url: request.url,
method: request.method,
},
},
});
// Return a user-friendly response
return new Response("Internal Server Error", {
status: 500,
headers: { "Content-Type": "text/plain" },
});
};
export const ErrorBoundary = () => (
<html>
<body>
<h1>Something went wrong</h1>
<p>Our team has been notified. Please try again later.</p>
</body>
</html>
);
This ensures every unhandled server error (in loaders, actions, or middleware) lands in your LightTrace project with full context: the request URL, method, cookies, and headers (sanitized of secrets by default).
Server vs. client error routing
A common source of confusion: should my Remix app send all errors to one DSN, or split them?
One DSN per app: simplest approach. Client and server errors arrive in the same project, labeled by runtime. Search by runtime:browser or runtime:server to filter.
Two DSNs (advanced): if your frontend and backend teams maintain separate projects, or if you need different retention policies. Create two LightTrace projects, pass the server DSN to the server SDK and the client DSN to the browser SDK.
For most Remix apps, one DSN is enough. The benefit of unified error tracking is seeing the relationship between server-side issues and client-side impact—if a loader times out, you see the timeout error and the resulting client error from the failed request.
Client errors are also captured automatically:
// This happens without code—Sentry catches all unhandled Promise rejections
// and global errors. But you can also capture manually:
try {
await fetchData();
} catch (error) {
Sentry.captureException(error);
}
Enriching errors with context and tags
Raw stack traces are useful; stack traces with context are actionable. Attach tags and extra data:
// In a loader
export const loader = async ({ request }) => {
const userId = await getSessionUserId(request);
Sentry.setUser({ id: userId });
Sentry.setTag("environment", "production");
Sentry.setContext("request", {
url: new URL(request.url).pathname,
route: "products.index",
});
try {
return json(await getProducts());
} catch (error) {
Sentry.captureException(error);
throw error;
}
};
In the LightTrace dashboard, you filter issues by tag, search by user ID, and replay the exact context where the error occurred.
Use how-to-debug-production-errors for deeper debugging workflows—source maps, breadcrumbs, and session replay all help you trace the full timeline of an error.
Testing your setup
Before deploying, verify errors reach LightTrace:
// Add a test route that throws
export const loader = async () => {
throw new Error("Test error from Remix");
};
Visit http://localhost:3000/test, and watch the error appear in your LightTrace project within seconds. Check that the stack trace is readable (if not, you need source maps—see the LightTrace docs).
For client errors, add a test button:
export default function TestError() {
return (
<button onClick={() => {
throw new Error("Test client error");
}}>
Trigger Client Error
</button>
);
}
Once you confirm errors arrive, remove the test route.
Release tracking and alerts
Tag errors with your release version so you know which deploys introduced regressions:
// In your Sentry.init call
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
release: process.env.COMMIT_SHA || "unknown",
environment: process.env.NODE_ENV,
});
In the LightTrace dashboard, what-is-error-tracking explains how to set up alert rules—get notified by email when a new issue is created or when error frequency spikes. Most teams start with "email me on new issues" and tune from there.
Email alerts require your LightTrace account email to be verified. Check your inbox and spam folder if you don't see the verification link.
Error tracking in Remix is straightforward once you understand the server/client split: initialize the SDK in both runtimes, wire handleError on the server, and let the client SDK handle global errors. The result is complete visibility into what breaks for users, grouped and prioritized by impact.
Start tracking errors in minutes
Start free with LightTrace today—set up error tracking for your Remix app in under 5 minutes.
If you're already using tanstack-start-error-handling or another full-stack framework, the pattern is the same: centralize error capture, attach context, and respond with insights instead of logs.