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

  1. Read the stack trace logged by console.error right above the 500 — it points at the exact page/endpoint line
  2. Add an Astro middleware (src/middleware.ts) with try/catch that returns a styled 500 page so users never see the bare response
  3. Verify production env vars, database URLs, and secrets are set before starting the server
  4. 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

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


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/f338197081900a35. Report an issue: GitHub.