withastro/astro · error · AstroError
The server entrypoint doesn't have a handler. Are you sure…
Error message
The server entrypoint doesn't have a handler. Are you sure this is the right file?
What it means
astro preview with the Node adapter dynamically imports the built SSR entry (preview.serverEntrypoint, normally dist/server/entry.mjs) and requires a named handler export that the adapter's server build produces. If the module imports fine but handler is not a function, preview refuses to start because it has nothing to serve requests with.
Solutions
- Delete dist/ and re-run astro build with the Node adapter configured (output: 'server') so the real entry with a named handler is regenerated
- If you use a custom server entry, export the handler: export const handler = (req, res) => app(req, res)
- Verify preview.serverEntrypoint / outDir were not redirected to a non-adapter file
Example fix
// before (custom entry) export default app; // after export const handler = (req, res) => app(req, res);
Defensive patterns
Strategy: validation
Validate before calling
import { pathToFileURL } from 'node:url';
const entry = 'dist/server/entry.mjs';
const mod: any = await import(pathToFileURL(entry).href);
if (typeof mod.handler !== 'function') {
throw new Error(`${entry} has no handler export; rebuild with the node adapter`);
} Type guard
type ServerModule = { handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void> };
function isServerModule(mod: unknown): mod is ServerModule {
return typeof (mod as any)?.handler === 'function';
} Prevention
- Run astro build immediately before astro preview in the same environment
- Clean dist/ whenever you change adapters or output settings
- If post-processing the entry, re-add a named handler export
When it happens
Trigger: The server entry was overwritten or replaced (custom build step, post-processing script) so it default-exports instead of exporting handler; previewing an outDir built by a different setup; a stale or partially overwritten dist where the entry no longer matches the adapter's expected shape.
Common situations: Custom bundling/patching of dist/server/entry.mjs; switching adapters or output modes without cleaning dist; entry templates that export app instead of handler.
Related errors
- The server entrypoint
- [preview] cannot preview your app.
- AdapterSupportOutputMismatch
- [@astrojs/alpinejs] entrypoint
- [@astrojs/node] Could not find the server directory
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/48c0d9eff6bccdba.
Report an issue: GitHub.
Appendix: source
Thrown at packages/integrations/node/src/preview.ts:18
import { fileURLToPath } from 'node:url';
import type { CreatePreviewServer } from 'astro';
import { AstroError } from 'astro/errors';
import { logListeningOn } from './log-listening-on.js';
import { createServer } from './standalone.js';
type ServerModule = typeof import('./server.js');
type MaybeServerModule = Partial<ServerModule>;
const createPreviewServer: CreatePreviewServer = async (preview) => {
let ssrHandler: ServerModule['handler'];
try {
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
const ssrModule: MaybeServerModule = await import(preview.serverEntrypoint.toString());
if (typeof ssrModule.handler === 'function') {
ssrHandler = ssrModule.handler;
} else {
throw new AstroError(
`The server entrypoint doesn't have a handler. Are you sure this is the right file?`,
);
}
} catch (err) {
if (
(err as any).code === 'ERR_MODULE_NOT_FOUND' &&
(err as any).url === preview.serverEntrypoint.href
) {
throw new AstroError(
`The server entrypoint ${fileURLToPath(
preview.serverEntrypoint,
)} does not exist. Have you ran a build yet?`,
);
} else {
throw err;
}
}
// If the user didn't specify a host, it will already have been defaulted toView on GitHub (pinned to 52e6c34790)