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

An API route endpoint (a `.ts`/`.js` file under `src/pages/` exporting `GET`/`POST`/etc.) ran its handler, but the resolved value is not a `Response` instance (it was falsy, or `response instanceof Response` is false). Astro endpoints are contract-bound to return exactly a `Response`. This is the `EndpointDidNotReturnAResponse` AstroError.

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 d081033d5f)

Solutions

  1. Ensure every code path in the endpoint handler returns a `Response` (check early returns and the final statement).
  2. Wrap data payloads: `return new Response(JSON.stringify(data), { headers: { 'content-type': 'application/json' } })` or `return Response.json(data)`.
  3. For empty/204-style replies use `return new Response(null, { status: 204 })`.
  4. Add a TypeScript return type of `Promise<Response>` so the compiler flags non-Response returns.

Example fix

// before
export const GET = async ({ url }) => {
  const data = await db.query();
  return data; // plain object -> throws
};

// after
export const GET = async ({ url }): Promise<Response> => {
  const data = await db.query();
  return Response.json(data);
};
Defensive patterns

Strategy: type-guard

Validate before calling

// Ensure every endpoint handler returns a Response
function assertResponse(value: unknown): Response {
  if (!(value instanceof Response)) {
    throw new Error('Endpoint handler must return a Response');
  }
  return value;
}

Type guard

const isResponse = (v: unknown): v is Response => v instanceof Response;

// Usage in a handler:
export const GET = async (ctx): Promise<Response> => {
  const res = await handler(ctx);
  return isResponse(res) ? res : Response.json(res);
};

Try / catch

// Wrap external calls so a thrown/non-Response result never escapes
export const GET = async (ctx): Promise<Response> => {
  try {
    const data = await loadData(ctx);
    return Response.json(data);
  } catch (e) {
    return Response.json({ error: String(e) }, { status: 500 });
  }
};

Prevention

When it happens

Trigger: The handler returns a plain object, array, string, number, `null`, or `undefined`; an async handler has a code path with no `return`; the handler returns a `Promise` that resolves to a non-Response; the handler returns a fetch result wrapper or custom class that isn't a `Response`.

Common situations: Returning JSON data directly (`return { ok: true }`) instead of wrapping it; forgetting `return` in an early-exit branch of an async handler; returning `Astro.redirect()`-like values from a helper that isn't a Response; migrating a page to an endpoint and forgetting the Response wrapping.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/452818059a0f309b. Report an issue: GitHub.