Yoga has experimental support for canceling the GraphQL execution.
In the real world, a lot of HTTP requests are dropped or canceled. This can happen due to a flakey
internet connection, navigation to a new view or page within a web or native app or the user simply
closing the app. In this case, the server can stop processing the request and save resources.
That is why Yoga comes with experimental support for canceling the GraphQL execution upon request
cancellation.
Getting started
To enable execution cancellation, you need to add the useExecutionCancellation plugin to your Yoga
instance.
Execution Cancellation configuration
import { createYoga, useExecutionCancellation } from 'graphql-yoga'import { schema } from './schema'// Provide your schemaconst yoga = createYoga({ plugins: [useExecutionCancellation()], schema})// Start the server and explore http://localhost:4000/graphqlconst server = createServer(yoga)server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql')})
That is all you need to do to enable execution cancellation in your Yoga server. Theoretically, you
can enable this and immediately benefit from it without making any other adjustments within your
GraphQL schema implementation.
If you want to understand how it works and how you can adjust your resolvers to properly cancel
pending promises (e.g. database reads or HTTP requests), you can continue with the next section.
How it works
The following example demonstrates how the execution cancellation works.
Simple GraphQL schema
type Query { user: User}type User { id: ID! name: String! bestFriend: User}
The Query.user resolver has a artificial delay of 10 seconds to simulate a long-running operation
e.g. a slow read from a database.
Full Yoga Example
import { createServer } from 'node:http'import { setTimeout as setTimeout$ } from 'node:timers/promises'import { createLogger, createSchema, createYoga, useExecutionCancellation } from 'graphql-yoga'const logger = createLogger('debug')const schema = createSchema({ typeDefs: /* GraphQL */ ` type Query { user: User } type User { id: ID! name: String! bestFriend: User } `, resolvers: { Query: { async user(_, __, { request }) { logger.info('resolving user') await setTimeout$(5000) logger.info('resolved user') return { id: '1', name: 'Chewie' } } }, User: { bestFriend() { logger.info('resolving user best friend') return { id: '2', name: 'Han Solo' } } } }})const yoga = createYoga({ plugins: [useExecutionCancellation()], schema, logging: logger})const server = createServer(yoga)server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql')})
With this server setup we are going to execute the following GraphQL query.
GraphQL Query for fetching user with best friend
query UserWithBestFriend { user { id name bestFriend { id name } }}
For your convenience, you can copy the following curl command to execute the query.
Curl Request for executing the operation
curl -g \ -X POST \ -H "content-type: application/json" \ -d '{"query":"{ user { id name bestFriend { id name } } }"}' \ "http://localhost:4000/graphql"
Execute this will give us the following server log output:
Server logs
DEBUG Parsing request to extract GraphQL parametersDEBUG Processing GraphQL ParametersINFO resolving userINFO resolved userINFO resolving user best friendDEBUG Processing GraphQL Parameters done.
Now, let’s cancel the request by closing the terminal or pressing Ctrl + C after the server shows
the INFO resolving user log.
This will now log the following.
Server logs for canceled request
DEBUG Parsing request to extract GraphQL parametersDEBUG Processing GraphQL ParametersINFO resolving userDEBUG Request abortedINFO resolved user
The interesting part is that now DEBUG Request aborted is shown. At this point, any further
GraphQL execution will be stopped and no other GraphQL resolvers will be invoked.
However, the INFO resolved user log is still showing up.
We can further adjust our server to cancel the artificial delay promise when the request is aborted
to avoid any further processing.
Altered server example with timer cleanup
import { createServer } from 'node:http'import { createLogger, createSchema, createYoga, useExecutionCancellation } from 'graphql-yoga'const logger = createLogger('debug')const schema = createSchema({ typeDefs: /* GraphQL */ ` type Query { user: User } type User { id: ID! name: String! bestFriend: User } `, resolvers: { Query: { async user(_, __, { request }) { logger.info('resolving user') await new Promise((resolve, reject) => { const timeout = setTimeout(resolve, 5000) request.signal.addEventListener('abort', () => { clearTimeout(timeout) reject(request.signal.reason) }) }) logger.info('resolved user') return { id: '1', name: 'Chewie' } } }, User: { bestFriend() { logger.info('resolving user best friend') return { id: '2', name: 'Han Solo' } } } }})// Provide your schemaconst yoga = createYoga({ plugins: [useExecutionCancellation()], schema, logging: logger})// Start the server and explore http://localhost:4000/graphqlconst server = createServer(yoga)server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql')})
After these adjustments, the timer will be cleaned up properly, and INFO resolved user, will no
longer show up in the logs.
As shown in the previous Selection, you can utilize the Yoga context object request.signal to
cancel pending asynchronous operations in your resolvers.
fetch example
In this example we just pass context.request.signal to the fetch call. This will cancel the fetch
request when the GraphQL request is canceled.
Resolver fetch cancellation example
import { createSchema, createYoga } from 'graphql-yoga'const yoga = createYoga({ schema: createSchema({ typeDefs: /* GraphQL */ ` type Query { greeting: String! } `, resolvers: { Query: { greeting: async (_, args, context) => { // This service does not exist const greeting = await fetch('http://localhost:9876/greeting', { signal: context.request.signal }).then(res => res.text()) return greeting } } } })})const server = createServer(yoga)server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql')})
Further Resources
The execution cancelation API is built on top of the AbortController and AbortSignal APIs.