withastro/astro · error
Server error
Error message
Server error
What it means
The @astrojs/node adapter wraps your built app so a Node HTTP server can serve it. When the underlying handler (page render, endpoint, or a handler you passed via middleware mode) throws or rejects, the wrapper logs `Could not render <url>` with the stack via console.error and, if headers are not yet sent, returns a bare `500 Server error`. It is the adapter's last-resort boundary; the body is empty because the app failed before it could render an error page. The real cause is the stack trace printed immediately above the response.
Source
Thrown at packages/integrations/node/src/middleware.ts:36
return async (...args) => {
// assume normal invocation at first
const [req, res, next, locals] = args;
// short circuit if it is an error invocation
if (req instanceof Error) {
const error = req;
if (next) {
return next(error);
} else {
throw error;
}
}
try {
await handler(req, res, next, locals);
} catch (err) {
logger.error(`Could not render ${req.url}`);
console.error(err);
if (!res.headersSent) {
res.writeHead(500, `Server error`);
res.end();
}
}
};
}
View on GitHub (pinned to 52e6c34790)
Solutions
- Read the stack trace logged by console.error right above the 500 — it points at the exact page/endpoint line
- Add an Astro middleware (src/middleware.ts) with try/catch that returns a styled 500 page so users never see the bare response
- Verify production env vars, database URLs, and secrets are set before starting the server
- If headersSent is true in logs, the page already streamed bytes — move the failing work earlier or stream defensively
Example fix
// before: page throws, users get bare '500 Server error'
const data = await db.query(`SELECT * FROM ${table}`);
// after: src/middleware.ts catches render errors globally
export const onRequest = async (context, next) => {
try {
return await next();
} catch (err) {
context.logger.error(`Render failed: ${context.url}`, err);
return new Response('Server error', { status: 500 });
}
}; Defensive patterns
Strategy: try-catch
Try / catch
// src/middleware.ts — app-wide error boundary before the adapter's bare 500
export const onRequest = async (context, next) => {
try {
return await next();
} catch (err) {
context.logger.error(`Could not render ${context.url.pathname}`, err);
return context.rewrite('/500'); // or new Response(errorPage, { status: 500 })
}
}; Prevention
- Add an error-handling middleware so users get a branded 500 page instead of the bare adapter response
- Fail fast on missing env vars/DB connections at server boot, not per-request
- Run `node dist/server/entry.mjs` locally before deploying to catch render errors the dev server masks
- Log url + stack (the adapter already does) and ship those logs to your monitoring
When it happens
Trigger: Any uncaught exception during SSR in the Node build: undefined property access in a page, missing environment variable, failed database call inside an endpoint, throw inside `locals` population — while the response headers have not been sent yet.
Common situations: Running `node dist/server/entry.mjs` in production and seeing blank 500s; errors only visible in server stdout; env vars present in dev but absent in prod; errors thrown after streaming starts (headersSent) where the socket simply dies instead.
Related errors
- [astro:actions] `defineAction()` unexpectedly used on the cl
- [astro:actions] `getActionContext()` unexpectedly used on th
- ActionCalledFromServerError
- ActionCalledFromServerError
- MissingGetFontFileRequestUrl
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/f338197081900a35.
Report an issue: GitHub.