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

  1. Wrap the payload: return Response.json(data) or new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })
  2. Make every code path return a Response, including early exits: return new Response(null, { status: 404 })
  3. Annotate handlers with TypeScript return types (Response | Promise<Response>) so the compiler catches bad returns
  4. 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

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


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)