DocumentationExecution Hooks

Execution Hooks and Async Cleanup

Execution hooks are experimental in GraphQL.js v17. The first hook, asyncWorkFinished, is useful for observing async cleanup after execution completes or aborts.

GraphQL execution can finish producing a response while background cleanup is still running. This often happens with async iterables, stream cancellation, or resolver-started side effects. The v17 hooks API lets hosts observe that final cleanup boundary.

asyncWorkFinished

Pass hooks through execute(), subscribe(), graphql(), or experimentalExecuteIncrementally().

import { execute } from 'graphql';
 
await execute({
  schema,
  document,
  hooks: {
    asyncWorkFinished({ validatedExecutionArgs }) {
      metrics.record('graphql.async_work_finished', {
        operationName: validatedExecutionArgs.operationName,
      });
    },
  },
});

This callback fires after GraphQL.js has stopped producing payloads and tracked async cleanup work has settled.

Tracking cleanup started by resolvers

Resolvers can register cleanup promises that are not returned directly from the resolver but still matter for lifecycle instrumentation.

const Query = new GraphQLObjectType({
  name: 'Query',
  fields: {
    ping: {
      type: GraphQLString,
      async resolve(_source, _args, _context, info) {
        const { track } = info.getAsyncHelpers();
        const cleanup = closeResourceSoon().catch(() => undefined);
 
        track([cleanup]);
        return 'pong';
      },
    },
  },
});

When track() is used, GraphQL.js includes those promises in the lifecycle seen by asyncWorkFinished.

Aborts and incremental delivery

Hooks are especially useful when execution is aborted or incremental delivery is used:

  • Abort may stop payload production before cleanup is complete.
  • Async iterator return() paths can continue after the response boundary.
  • Deferred work can leave short-lived cleanup tasks after the final patch.

Pair hooks with Handling Abort Signals for timeout and cancellation instrumentation.

Stability expectations

The hooks surface is experimental in v17 beta and may expand. Build host-level instrumentation so additional hook fields can be adopted without breaking existing behavior.

For application-level telemetry, keep hook handlers idempotent and resilient to errors.

Safe hook handlers

Treat hook handlers as observability code, not request-critical code. If a metrics backend fails, do not break GraphQL execution.

function safeAsyncWorkFinishedHook(handler) {
  return (payload) => {
    try {
      handler(payload);
    } catch (error) {
      logger.warn({ error }, 'asyncWorkFinished hook failed');
    }
  };
}
 
await execute({
  schema,
  document,
  hooks: {
    asyncWorkFinished: safeAsyncWorkFinishedHook(({ validatedExecutionArgs }) => {
      metrics.record('graphql.async_work_finished', {
        operationName: validatedExecutionArgs.operationName,
      });
    }),
  },
});

This pattern helps avoid cascading failures where telemetry outages impact query handling.