withastro/astro · error · AstroError
EndpointDidNotReturnAResponse
EndpointDidNotReturnAResponse
Error message
An endpoint must return either a `Response`, or a `Promise` that resolves with a `Response`.
What it means
Astro API routes (endpoints) must return a Response instance from every handler. After awaiting the handler, the endpoint runner checks response instanceof Response; returning undefined, a plain object, a string, or any other value throws EndpointDidNotReturnAResponse. (A handler that exists but is not a function at all is a different, logged 500 path just above.)
Solutions
- Wrap the payload: return Response.json(data) or new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })
- Make every code path return a Response, including early exits: return new Response(null, { status: 404 })
- Annotate handlers with TypeScript return types (Response | Promise<Response>) so the compiler catches bad returns
- If you need a passthrough, return fetch(...) directly since that already resolves to a Response
Example fix
// before
export const GET = async () => {
const data = await getPosts();
return data;
};
// after
export const GET = async () => {
const data = await getPosts();
return Response.json(data);
}; Defensive patterns
Strategy: type-guard
Validate before calling
export const GET = async () => {
const data = await getPosts();
const body = JSON.stringify(data);
return new Response(body, { headers: { 'Content-Type': 'application/json' } });
}; Type guard
function isResponse(value: unknown): value is Response {
return value instanceof Response;
} Try / catch
Wrap handler bodies in try/catch and always end with a Response, e.g. catch (e) { return new Response('Server error', { status: 500 }); } — the runtime throws EndpointDidNotReturnAResponse only when a non-Response escapes, so guaranteeing the return type in every branch (including catch) fully avoids it. Prevention
- Annotate every handler: export const GET: APIRoute = async ({}) => { ... } (APIRoute forces a Response return)
- Use Response.json(data) instead of returning raw objects
- Lint for 'return;' with no argument inside endpoint files
When it happens
Trigger: export const GET = () => someData returning raw JSON data; an early 'return;' with no value; returning JSON.stringify(data) (a string, not a Response); a helper that returns parsed data instead of a fetch Response; an async branch that forgets to wrap the result.
Common situations: Migrating Express-style res.json() handlers to Astro endpoints; forgetting the Response wrapper in one branch of a conditional; returning the result of a data-layer function directly; returning a fetch() result object from an intermediate helper that already consumed it.
Related errors
- OnlyResponseCanBeReturned
- MiddlewareNotAResponse
- ResponseSentError
- ⚠️ Astro could not optimize image
- ⚠️ Astro expected an SVG for
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/452818059a0f309b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/astro/src/runtime/server/endpoint.ts:64
: ''),
);
// No handler matching the verb found, so this should be a
// 404. Should be handled by 404.astro route if possible.
return new Response(null, { status: 404 });
}
if (typeof handler !== 'function') {
logger.error(
'router',
`The route "${
url.pathname
}" exports a value for the method "${method}", but it is of the type ${typeof handler} instead of a function.`,
);
return new Response(null, { status: 500 });
}
let response = await handler.call(mod, context);
if (!response || response instanceof Response === false) {
throw new AstroError(EndpointDidNotReturnAResponse);
}
// Endpoints explicitly returning 404 or 500 response status should
// NOT be subject to rerouting to 404.astro or 500.astro.
if (state && REROUTABLE_STATUS_CODES.includes(response.status)) {
state.skipErrorReroute = true;
}
if (method === 'HEAD') {
// make sure HEAD responses doesnt have body
return new Response(null, response);
}
return response;
}
View on GitHub (pinned to 52e6c34790)