Handling Abort Signals
Abort signal support is available in GraphQL.js v17 and newer. It is a GraphQL.js-specific runtime API, not a GraphQL specification feature.
Long-running GraphQL operations often start other asynchronous work: database
queries, HTTP requests, async iterators, loaders, and subscription streams. In
v16, stopping the outer request did not give GraphQL.js a standard way to tell
that work to stop. In v17, execution accepts an AbortSignal and passes a
resolver-scoped signal through GraphQLResolveInfo.
Passing a signal to execution
Pass abortSignal to graphql(), execute(), subscribe(), or
experimentalExecuteIncrementally().
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, the returned promise rejects. The abort reason becomes the rejection cause when possible.
Using the signal in resolvers
Resolvers receive a signal through info.getAbortSignal(). Pass it to any API
that supports 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();
},
},
},
});getAbortSignal() can return undefined when the caller did not provide a
signal. Code that passes it to Web APIs such as fetch() can usually pass
undefined directly.
For APIs that do not support AbortSignal, check the signal before starting
work and subscribe to the abort event:
async function loadWithCleanup(info) {
const abortSignal = info.getAbortSignal();
if (abortSignal?.aborted) {
throw abortSignal.reason;
}
const cleanup = startExpensiveWork();
abortSignal?.addEventListener(
'abort',
() => {
cleanup.stop();
},
{ once: true },
);
return cleanup.result;
}Wiring HTTP request life cycles
Most servers already have a request lifecycle signal (client disconnect, gateway timeout, or framework cancellation token). Bridge that signal to GraphQL.js so resolver cancellations are aligned with 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 avoids doing expensive resolver work after the client is already gone.
Handling aborted execution
GraphQL.js rejects with AbortedGraphQLExecutionError when an operation is
aborted after execution 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.
Cleanup after execution
Execution stops producing the response as soon as it is aborted, but async
cleanup may still be running. v17 exposes an experimental asyncWorkFinished
hook for instrumentation that needs to observe when tracked async work has
settled.
await execute({
schema,
document,
abortSignal,
hooks: {
asyncWorkFinished() {
metrics.increment('graphql.async_work_finished');
},
},
});This hook is useful when resolvers return async iterables or when aborting
execution triggers iterator return() cleanup.
Resolvers can also register async work that GraphQL.js should track before that hook fires. This is mainly for cleanup that is started by a resolver but is not itself returned from the resolver.
async resolve(_source, _args, _context, info) {
const { track } = info.getAsyncHelpers();
const cleanup = closeConnectionLater().catch(() => undefined);
track([cleanup]);
return 'ok';
}If you are awaiting work in the resolver, return or await it normally. Use
track() for side-effect cleanup that would otherwise be invisible to
GraphQL.js. The related promiseAll() helper wraps Promise.all() so rejected
branches are tracked consistently when the returned promise is awaited.
Practical guidance
- Treat abort as best-effort cancellation. A resolver 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.