remix-run/react-router · error
() call aborted without an `AbortSignal.reason
Error message
${method}() call aborted without an `AbortSignal.reason`: ${request.method} ${request.url} What it means
createStaticHandler's query()/queryRoute() aborts by re-throwing the AbortSignal's reason so callers can tell intentional cancellation apart from crashes. If the signal was aborted with no reason (a bare controller.abort()), throwStaticHandlerAbortedError (lib/router/router.ts:5092) throws this error because the framework cannot classify the abort.
Solutions
- Always abort with a reason: controller.abort(new Error('timeout')) or new DOMException('timeout', 'TimeoutError')
- Check for double-abort: guard with if (signal.aborted) return before calling abort() again
- Ensure only one owner aborts a shared signal
- Catch the re-thrown reason in your handler and map known reasons to proper responses
Example fix
// before
setTimeout(() => controller.abort(), 5000)
// after
setTimeout(() => controller.abort(new Error('SSR request timeout')), 5000) Defensive patterns
Strategy: validation
Validate before calling
// Single-owner abort helper that always attaches a reason
function abortWithReason(ctrl: AbortController, reason: unknown) {
if (ctrl.signal.aborted) return;
ctrl.abort(reason instanceof Error ? reason : new Error(String(reason)));
}
setTimeout(() => abortWithReason(controller, 'SSR timeout'), 5000); Try / catch
try {
const ctx = await staticHandler.query(request);
} catch (e) {
if (request.signal.aborted) {
// distinguish intentional aborts: reason is re-thrown by the handler
return new Response('Aborted: ' + String(request.signal.reason), { status: 499 });
}
throw e;
} Prevention
- Never call controller.abort() without an argument
- Guard double aborts with if (signal.aborted) return
- Give each in-flight SSR request its own controller; abort only from its owner
- Log request.signal.reason in error reporting to classify aborts
When it happens
Trigger: SSR request handling with a bare timeout abort: setTimeout(() => controller.abort(), 5000); calling abort() twice where the second call has no reason; a shared signal aborted by another consumer (e.g., a competitor fetch wins and aborts the common signal); environments whose abort implementation does not populate signal.reason.
Common situations: Node/edge SSR handlers adding request timeouts; React streaming SSR where an earlier render pass is aborted; Promise.race patterns that abort a shared controller; older fetch polyfills in Node that do not set signal.reason.
Related errors
- A meta object uses an invalid tagName
- Cannot write to a destroyed or ended writable stream
- Failed to clone server response
- Invalid redirect location
- Missing body in server response
AI-assisted analysis of remix-run/react-router@7aea711dd1 (2026-08-18).
Data as JSON: /api/errors/559a2270db71da2a.
Report an issue: GitHub.
Appendix: source
Thrown at packages/react-router/lib/router/router.ts:5079
return {
...handlerContext,
statusCode: isRouteErrorResponse(error) ? error.status : 500,
errors: {
[errorBoundaryId]: error,
},
};
}
function throwStaticHandlerAbortedError(
request: Request,
isRouteRequest: boolean,
) {
if (request.signal.reason !== undefined) {
throw request.signal.reason;
}
let method = isRouteRequest ? "queryRoute" : "query";
throw new Error(
`${method}() call aborted without an \`AbortSignal.reason\`: ${request.method} ${request.url}`,
);
}
function isSubmissionNavigation(
opts: BaseNavigateOrFetchOptions,
): opts is SubmissionNavigateOptions {
return (
opts != null &&
(("formData" in opts && opts.formData != null) ||
("body" in opts && opts.body !== undefined))
);
}
function defaultNormalizePath(request: Request): Path {
let url = new URL(request.url);
return {
pathname: url.pathname,View on GitHub (pinned to 7aea711dd1)