🎉 Explore GraphQLConf 2026 • May 19-21 • Fremont, CA • View the schedule
DocumentationAbort Signals

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 abortSignal to execute(), subscribe(), graphql(), or experimentalExecuteIncrementally() 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.