Handling Abort Signals
Abort signal support is available in GraphQL.js v17 and newer. It is a GraphQL.js runtime API, not GraphQL syntax or a transport protocol.
What the specification says
The GraphQL specification mentions cancellation in a narrow execution case.
During ordinary execution, when a non-null execution error propagates to a
parent response position, sibling response positions that have not executed or
yielded a value
may be cancelled
to avoid unnecessary work. The
conformance appendix
makes lowercase key words in normative portions of the specification carry
their RFC 2119 meaning, so this is a normative MAY. It permits cancellation;
it does not require every implementation to cancel work in that case.
The subscription algorithms also describe cancellation of response streams and source streams. Those algorithms are still about GraphQL execution and stream lifecycle, not about a user or host cancelling a request that is already in flight.
The specification does not define a cancellation primitive or a transport cancellation protocol.
What GraphQL.js v17 adds
GraphQL.js v17 exposes cancellation for two related situations:
- Internally, GraphQL.js can signal work that no longer contributes to the returned result, such as work from response positions cancelled under the specification’s execution rules.
- Externally, a host can pass
abortSignaltoexecute(),subscribe(),graphql(), orexperimentalExecuteIncrementally()to cancel an issued request while it is in flight.
Both cases matter because long-running GraphQL operations often start other
asynchronous work: database queries, HTTP requests, async iterators, loaders,
and subscription streams. In v16, GraphQL.js had no standard way to ask that
work to stop. In v17, GraphQL.js uses AbortSignal as its JavaScript runtime
API for cancellation.
GraphQL.js v17 uses the same resolver-scoped signal for internal cancellation
and external request cancellation. Resolvers read that shared signal with
info.getAbortSignal() and pass it to downstream APIs that support
cancellation.
Abort signals are cooperative. They let GraphQL.js and resolvers pass a cancellation request to downstream work, but JavaScript cannot force an arbitrary promise, database driver, HTTP client, or async iterator to stop. The downstream API has to accept the signal and honor it.
GraphQL.js does not currently provide fine-grained per-field or per-branch
abort signals. Resolvers in an operation share one resolver AbortSignal. For
internally cancelled portions of an operation, GraphQL.js aborts that shared
signal when the result that will actually be returned has finished, notifying
pending resolver work together. An external abort also aborts the same shared
resolver signal. This coarseness may change in future versions.
Using the signal in resolvers
Resolvers obtain the abort signal via info.getAbortSignal(). Pass that
signal to downstream APIs that support cancellation.
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: User,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
async resolve(_source, args, _context, info) {
const abortSignal = info.getAbortSignal();
const response = await fetch(`https://users.example/${args.id}`, {
signal: abortSignal,
});
return response.json();
},
},
},
});As noted, this signal can be triggered by GraphQL.js itself. For example,
suppose a query selects user { profile recommendations }, profile is
non-null, and the recommendations resolver starts a slow downstream request.
If profile throws and the error bubbles so that user becomes null, the
recommendations result can no longer appear in the response. GraphQL.js does
not currently create a separate signal for that one sibling branch. Instead,
when the response that will actually be returned has finished, GraphQL.js
aborts the shared resolver signal, so any still-pending resolver work that
honors the signal can stop together.
For work that does not accept AbortSignal, check the signal before starting
and again between expensive steps. This is useful even for synchronous chunks:
JavaScript cannot interrupt code that is already running, but the next check can
avoid starting more work.
async function loadReport(info) {
const abortSignal = info.getAbortSignal();
if (abortSignal?.aborted) {
throw abortSignal.reason;
}
const rows = await loadReportRows();
if (abortSignal?.aborted) {
throw abortSignal.reason;
}
const totals = calculateTotals(rows);
if (abortSignal?.aborted) {
throw abortSignal.reason;
}
return formatReport(totals);
}If the API exposes its own cancellation method, connect the abort event to that method:
async function loadReportFromJob(info) {
const abortSignal = info.getAbortSignal();
if (abortSignal?.aborted) {
throw abortSignal.reason;
}
const job = startReportJob();
abortSignal?.addEventListener(
'abort',
() => {
job.cancel();
},
{ once: true },
);
return job.result;
}Here startReportJob() represents an async API that does not accept
AbortSignal directly, but does return an object with a result promise and a
cancel() method.
Passing an external signal to execution
GraphQL.js also lets a host cancel an issued request while it is in flight.
Pass abortSignal to graphql(), execute(), subscribe(), or
experimentalExecuteIncrementally(). If that external signal is aborted,
GraphQL.js aborts the same resolver signal exposed through
info.getAbortSignal().
import { execute, parse } from 'graphql';
const controller = new AbortController();
const document = parse(`
query User($id: ID!) {
user(id: $id) {
id
name
}
}
`);
const resultPromise = execute({
schema,
document,
variableValues: { id: '123' },
abortSignal: controller.signal,
});
setTimeout(() => {
controller.abort(new Error('Request timed out'));
}, 500);
const result = await resultPromise;If the signal is aborted before execution finishes, asynchronous execution rejects. The abort reason becomes the rejection cause when possible.
Wiring HTTP request life cycles
Most servers already know when a request is no longer useful: the client disconnects, a gateway timeout fires, or a framework cancellation token is triggered. Bridge that lifecycle to GraphQL.js so resolver cancellation follows request cancellation.
const controller = new AbortController();
req.on('close', () => {
controller.abort(new Error('Client disconnected'));
});
const result = await execute({
schema,
document,
variableValues,
contextValue,
abortSignal: controller.signal,
});This helps avoid expensive resolver work after the client is already gone.
Handling aborted execution
GraphQL.js rejects with AbortedGraphQLExecutionError when execution is
aborted after it has started. The error exposes the best partial result
GraphQL.js can still produce.
import { AbortedGraphQLExecutionError, execute } from 'graphql';
try {
const result = await execute({
schema,
document,
abortSignal,
});
return result;
} catch (error) {
if (error instanceof AbortedGraphQLExecutionError) {
const partialResult = await error.abortedResult;
logger.info({ partialResult }, 'GraphQL execution aborted');
}
throw error;
}abortedResult may be either a result or a promise for a result. For
incremental delivery, it may contain the initial incremental result if that was
already available.
Observing async cleanup
An abort can stop GraphQL.js from producing more response data before every tracked async task has settled. Cleanup can continue after the response boundary, especially when resolvers use async iterators or when downstream libraries perform their own shutdown work.
Use the experimental asyncWorkFinished execution hook when a host needs to
observe that boundary. See Execution Hooks for
examples of logging cleanup completion, delaying response delivery until
tracked work settles, and tracking resolver-started async work.
Practical guidance
- Treat abort as cooperative cancellation. A resolver or downstream client that ignores the signal may keep doing work outside GraphQL.js.
- Pass the signal to downstream clients early, before starting expensive work.
- Avoid swallowing abort errors in resolvers. Let GraphQL.js stop the operation.
- Keep request timeouts at the server or transport layer, and connect those
timeouts to an
AbortController. - Do not expose abort signals in the GraphQL schema. They are a JavaScript runtime concern, not client query syntax.