Loader errors render the root ErrorBoundary but no stack trace appears in deployment logs
0 reputation · 14 May 2024, 13:30 UTC
0 reputation · 14 May 2024, 13:30 UTC
On a Remix app deployed behind a serverless platform, a thrown loader error renders the root ErrorBoundary and returns a 500, yet the platform's function logs show nothing about the exception. The goal is to reliably capture the original stack trace for every loader/action failure in production.
Remix itself does not log thrown errors automatically; the adapter's createRequestHandler is responsible for surfacing them. It is unclear whether the missing logs mean the server entry point is not forwarding errors, or whether the deployment replaced or misconfigured the adapter handler. There is also the streaming case: with defer, a rejected deferred loader arrives as a 200 with the error boundary inside the stream, so the failure may never appear as an error status at all.
Assume Remix v2 with a custom entry.server; exact behavior should be confirmed against the installed version.
32275 reputation · 15 May 2024, 00:29 UTC
errorHandler, log the stack trace, re‑throw, and inject the CDN request‑ID into the page. For deferred loaders add a global unhandledRejection listener that logs rejected promises before they reach the stream.cf-ray, x-request-id, x-vercel-id) in entry.server.js, include it in the console log, and inject it into the rendered HTML as a meta tag or hidden span. This allows you to match the 500 response to the log line by ID.console.error entry that contains the stack trace and request ID. Verify that the same ID appears in the rendered page’s meta tag.Remix does not automatically surface thrown errors to the platform’s log stream. The adapter’s createRequestHandler can forward an error to a custom errorHandler, but if that handler is omitted or the platform rewrites entry.server, the error never reaches the logs. Deferred loaders resolve later and therefore never hit the request handler, so their rejections are silent unless explicitly caught.
createRequestHandler accepts an errorHandler option that receives the error, request, and response.errorHandler preserves the ErrorBoundary rendering.errorHandler; they must be caught in the loader or via process.on('unhandledRejection').request.headers and can be forwarded to logs and the HTML.entry.server.js:
import { createRequestHandler } from '@remix-run/server-runtime';
const handler = createRequestHandler({
getLoadContext() { return {}; },
errorHandler(error, request, response) {
const reqId = request.headers.get('cf-ray') || request.headers.get('x-request-id') || request.headers.get('x-vercel-id') || 'unknown';
console.error('[REMIX ERR]', reqId, error.stack || error.message);
throw error;
},
});
export default async function remixEntry(request, env, ctx) {
try {
return await handler(request, env, ctx);
} catch (e) {
if (!e.response) throw e;
return e.response;
}
}
In your root Document component, add a meta tag or hidden span that reads the ID from the load context or from request.headers.get('cf-ray'). Example:
export default function Document({ children }) {
const reqId = useLoaderData();
return (
<html>
<head>
<meta name="request-id" content={reqId} />
...
</head>
<body>{children}</body>
</html>
);
}
Add a global listener in entry.server.js that logs any unhandled promise rejections before they reach the stream.
process.on('unhandledRejection', (reason, promise) => {
console.error('[UNHANDLED REJECTION]', reason.stack || reason);
});
throw new Error('Test stack');.[REMIX ERR] and includes the stack trace.Which CDN or serverless platform are you using (e.g., Cloudflare Workers, Vercel Edge, Netlify Functions)? Knowing the exact request‑ID header name helps tailor the code snippet.
By explicitly logging inside a custom errorHandler and correlating a CDN request ID in both the log and the HTML, you can reliably trace every loader/action failure in production, even when using deferred streams.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.