---
title: What changed in GraphQL.js v17
sidebarTitle: v16 to v17
---

import { Callout } from 'nextra/components';

# What Changed in GraphQL.js v17

<Callout type="info">
  GraphQL.js v17 is currently available as `17.0.0-beta.1`. This guide
  describes migration-impacting changes from the v16 stable line to the v17
  beta line.
</Callout>

GraphQL.js v17 keeps the core programming model: build a schema, parse a
document, validate it, and execute it. Most changes make boundaries more
explicit. Stable single-result execution is separate from experimental
incremental delivery, input coercion is split from diagnostic validation, and
development checks are opt-in. Host integration features such as harnesses,
abort signals, and execution hooks are explicit GraphQL.js runtime APIs.

## Reading the Labels

Migration items below use these labels:

- **Breaking change:** v16 code may need to change before it runs on v17.
- **Behavioral tightening:** v17 validates or reports a case more precisely.
- **Deprecation:** the v16 API still works in v17, but should be migrated
  before v18.
- **New stable API:** a new public API that can be adopted independently.
- **Experimental or opt-in:** available in v17, but proposal-backed or outside
  the default execution path.

## Platform and Package Shape

### Node.js and TypeScript

**Breaking change.** GraphQL.js v17 requires Node.js 22, 24, 25, or 26 and
later:

```json
{
  "engines": {
    "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0"
  }
}
```

Upgrade Node.js before upgrading GraphQL.js. This separates runtime and
package-manager errors from GraphQL.js migration errors.

**Breaking change.** The published type definitions target TypeScript 4.4 and
newer.

### Conditional exports

**Breaking change.** v17 uses package `exports` and modern package conditions
to select the right build. Use public entry points such as `graphql`,
`graphql/execution`, `graphql/language`, `graphql/type`, `graphql/utilities`,
and `graphql/validation`.

Deep imports into GraphQL.js internals may still work in some environments, but
they are not officially supported. Prefer the public entry points above for
application and library code.

**Breaking change.** The deprecated `graphql/subscription` compatibility
subpath is gone. Import subscription APIs from `graphql` or
`graphql/execution`.

```diff
- import { subscribe } from 'graphql/subscription';
+ import { subscribe } from 'graphql/execution';
```

### Development mode

**Breaking change.** Development mode is disabled by default and no longer
depends on `NODE_ENV`. Enable it with the `development` package condition or by
calling `enableDevMode()` during application startup.

```js
import { enableDevMode, isDevModeEnabled } from 'graphql';

if (process.env.NODE_ENV === 'development') {
  enableDevMode();
}

console.log(isDevModeEnabled());
```

Development mode currently helps diagnose accidental use of multiple GraphQL.js
module instances. See [Development Mode](/docs/development-mode) for runtime
and bundler setup.

## Request Pipeline and Harnesses

### `graphql()` and `graphqlSync()`

**New stable API.** The high-level `graphql()` and `graphqlSync()` APIs still
use the object-argument form from v16. In v17, that object can also carry
parser options, validation options, execution options, `hideSuggestions`,
`abortSignal`, hooks, and a custom harness.

```js
const result = await graphql({
  schema,
  source,
  variableValues,
  operationName: 'Viewer',
  hideSuggestions: true,
  abortSignal,
});
```

This does not make `graphql()` a framework. It remains the convenience API for
a single-result "parse, validate, execute" request. The new arguments let
simple hosts use common v17 options without rebuilding the whole pipeline.

### GraphQL Harness

**New stable API.** `GraphQLHarness` lets hosts replace the parse, validate,
execute, and subscribe phases used by `graphql()` and `graphqlSync()`.

The harness is modeled after Envelop-style plugin pipelines. It brings the
broader phase types used by that ecosystem closer to the reference
implementation, so frameworks can accept a custom harness and plugin systems
can interoperate around a shared request-pipeline shape.

For example, `GraphQLParseFn` can return either a parsed `DocumentNode` or a
promise for one, even though the built-in `parse()` function is synchronous.
The harness `execute` function follows `execute()` and does not include the
experimental incremental delivery return type.

Use [GraphQL Harness](/docs/graphql-harness) for examples, and call
`experimentalExecuteIncrementally()` directly when an operation may use
`@defer` or `@stream`.

## Execution and Incremental Delivery

### Single-result execution

**Breaking change.** `execute()` is now the stable single-result executor. It
does not support incremental delivery. If a schema or operation opts into
`@defer` or `@stream`, execute it with `experimentalExecuteIncrementally()`
instead.

```diff
- import { execute } from 'graphql';
+ import { experimentalExecuteIncrementally } from 'graphql';
```

This keeps the `execute()` contract simple: callers receive one
`ExecutionResult`, or a promise for one.

**Breaking change.** Incremental execution no longer uses the old
`singleResult` discriminator. Remove branches that check for `singleResult`.

### Incremental delivery

**Experimental or opt-in.** `experimentalExecuteIncrementally()` returns either
a normal `ExecutionResult` or an object with `initialResult` and an async
iterator of `subsequentResults`.

```js
const result = await experimentalExecuteIncrementally({ schema, document });

if ('initialResult' in result) {
  send(result.initialResult);

  for await (const subsequentResult of result.subsequentResults) {
    send(subsequentResult);
  }
} else {
  send(result);
}
```

**Experimental or opt-in.** `legacyExecuteIncrementally()` remains available
for hosts that still need the older incremental delivery payload shape. The
legacy shape identifies deferred and streamed payloads with fields such as
`path` and optional `label`, and can duplicate field data across payloads. The
current experimental shape registers pending work by `id` and reports
completion with `completed` entries.

Schema setup, directive validation, result shapes, and transport guidance are
covered in [Defer and Stream](/docs/defer-stream).

### Resolver return values

**New stable API.** List fields can resolve to async iterables. This is useful
for values that naturally arrive over time and is especially relevant when a
host opts into `@stream`.

### Custom execution helpers

**New stable API.** v17 exposes the helper boundary used by `execute()` itself.
`validateExecutionArgs()` validates and normalizes `ExecutionArgs`, including
schema checks, operation selection, variable coercion, fragment information,
default resolvers, and execution options. It returns either
`ValidatedExecutionArgs` or a list of `GraphQLError` values.

Then use the root selection set helper that matches the executor you are
building:

- `executeRootSelectionSet()` for stable single-result execution.
- `experimentalExecuteRootSelectionSet()` for current incremental delivery.
- `legacyExecuteRootSelectionSet()` for the legacy incremental payload shape.

These helpers do not replace operation validation with `validate()`. They are
for hosts that already parsed and validated a document and need a custom
execution function.

The native `execute()` and `subscribe()` functions preserve synchronous
results when execution completes synchronously; they do not wrap every result
in a promise. Custom wrappers that want the same behavior need to handle both
sync and async paths. See
[Advanced Execution Pipelines](/docs/advanced-execution-pipelines) for complete
examples.

## Subscriptions and Source Event Streams

### Subscription return type

**Breaking change.** `subscribe()` returns a `PromiseOrValue` in v17 instead
of always returning a promise. Existing `await subscribe(args)` code continues
to work, but TypeScript code that assumed a promise return type must be updated
to handle the synchronous path.

**Breaking change.** `subscribe()` does not support incremental delivery. If a
fragment is shared between queries and subscriptions, use the `if` argument on
`@defer` or `@stream` to disable incremental behavior in subscription
operations.

### Lower-level subscription helpers

**Breaking change.** `createSourceEventStream()` now accepts only
`ValidatedSubscriptionArgs`. If you call it directly, call
`validateSubscriptionArgs()` first and handle validation errors before passing
the result onward. Use `subscribe()` when you want GraphQL.js to run the full
subscription pipeline.

`validateSubscriptionArgs()` builds on `validateExecutionArgs()` and additionally
asserts that the selected operation is a subscription.

**New stable API.** `mapSourceToResponseEvent()` maps a subscription source
stream to execution results. Its third argument is a `RootSelectionSetExecutor`,
which customizes how each source event is executed after the source stream has
already been created. If you omit it, GraphQL.js uses the default
subscription-event executor.

See [Advanced Execution Pipelines](/docs/advanced-execution-pipelines) for
complete helper examples.

## Abort Signals

**Experimental or opt-in.** GraphQL.js v17 adds `AbortSignal` support as its
JavaScript runtime API for cancellation.

The GraphQL specification discusses cancellation in narrower execution cases.
During non-null error propagation, sibling response positions that have not
executed or yielded a value
[may be cancelled](https://spec.graphql.org/draft/#sec-Errors-and-Non-Null-Types)
to avoid unnecessary work; in the specification's
[normative sections](https://spec.graphql.org/draft/#sec-Appendix-Conformance),
lowercase `may` has RFC 2119 `MAY` force. The subscription algorithms also
describe cancelling response and source streams. The specification does not
define a user or host cancelling an already issued query or mutation request.

GraphQL.js also accepts an external `abortSignal` on `graphql()`, `execute()`,
`subscribe()`, and `experimentalExecuteIncrementally()`. Resolvers read the
resolver-scoped signal with `info.getAbortSignal()` and pass it to downstream
APIs that support cancellation.

At this time, GraphQL.js does not expose fine-grained per-field cancellation.
Resolvers in an operation share one signal. For internally cancelled portions
of an operation, GraphQL.js aborts that shared resolver signal when the result
that will actually be returned has finished, notifying still-pending resolver
work together. This may change in future versions.

See [Abort Signals](/docs/abort-signals) for resolver examples, HTTP request
lifecycle wiring, and aborted execution handling.

## Execution Hooks

**Experimental or opt-in.** Execution hooks are separate from abort signals.
They let a host observe execution lifecycle boundaries; they do not force
JavaScript work to stop.

The first hook is `asyncWorkFinished`. It fires after GraphQL.js has stopped
producing payloads and all tracked async execution work has settled. This is
useful for cleanup, logging, telemetry, tests that need a strong lifecycle
boundary, or hosts that choose to delay returning a result until tracked async
work is finished.

Resolvers can use `info.getAsyncHelpers()` to make additional work visible to
that tracking. See [Execution Hooks](/docs/execution-hooks) for examples.

## Input Coercion, Defaults, and Custom Scalars

### Default values

**Behavioral tightening.** Argument, input-field, and directive-argument
defaults are now validated as part of schema validation performed by
`validateSchema()`. Invalid defaults that v16 could leave latent now make the
schema invalid with a targeted validation error.

**Deprecation.** `defaultValue` is the legacy programmatic default format. It
represents an already-coerced JavaScript value and remains available as a
migration bridge.

**New stable API.** Prefer the new `default` model for programmatic schemas.
Use `default: { value }` when you have the raw JavaScript input value before
coercion. Use `default: { literal }` when you have the GraphQL literal. When
GraphQL.js builds a schema from SDL, it now uses the literal form internally.

```js
const field = {
  type: GraphQLString,
  args: {
    format: {
      type: GraphQLString,
      default: { value: 'short' },
    },
  },
};
```

This fixes subtle cases where introspection could report default values
incorrectly from an already-coerced internal value.

See [Passing Arguments](/docs/passing-arguments) and
[Mutations and Input Types](/docs/mutations-and-input-types).

### Coercion and validation helpers

**Behavioral change.** `coerceInputValue()` and `coerceInputLiteral()` now
return `undefined` when coercion fails. They are optimized for callers that
want either a coerced value or failure.

**New stable API.** Use `validateInputValue()` and `validateInputLiteral()` when
you need diagnostic errors. Use `valueToLiteral()` when converting an external
JavaScript input value into a GraphQL literal.

**New stable API.** `replaceVariables()` replaces variables inside complex
scalar literals. GraphQL.js calls it automatically during execution. If you use
literal coercion helpers directly outside execution, call `replaceVariables()`
yourself before coercing literals that may contain variables.

### Custom scalar method names

**Deprecation.** v17 introduces scalar method names that match the GraphQL
coercion model:

| v16 name | v17 name | Purpose |
| --- | --- | --- |
| `serialize` | `coerceOutputValue` | Convert resolver values into response values. |
| `parseValue` | `coerceInputValue` | Convert variable values into internal values. |
| `parseLiteral` | `coerceInputLiteral` | Convert GraphQL literals into internal values. |
| `astFromValue()` | `valueToLiteral()` | Convert external input values into GraphQL literals. |

The v16 scalar method names still work in v17 and are deprecated for removal in
v18. See [Advanced Custom Scalars](/docs/advanced-custom-scalars).

## Variable Values and Resolver Info

**Breaking change.** `getVariableValues()` now returns `{ variableValues }` on
success. That value contains source information and coerced runtime values.

```diff
- const { coerced } = getVariableValues(schema, variableDefinitions, inputs);
+ const { variableValues } = getVariableValues(schema, variableDefinitions, inputs);
+ const coerced = variableValues.coerced;
```

This matters because v17 supports fragment-local variables and more precise
default handling. Passing only the coerced object loses information about where
values came from.

**Breaking change.** `info.variableValues` follows the same model. Use
`info.variableValues.coerced` for runtime values inside resolvers.

**Behavioral change.** Resolver argument maps and variable maps may use
null-prototype objects. Use `Object.hasOwn(obj, key)` or direct property access
instead of methods inherited from `Object.prototype`.

**New stable API.** `GraphQLResolveInfo` adds `getAbortSignal()` and
`getAsyncHelpers()` for the abort-signal and execution-hook APIs.

## Schema, Type System, Directives, and Introspection

### Schema validation

**Behavioral tightening.** `GraphQLSchema.toConfig().assumeValid` now preserves
the original `assumeValid` setting instead of changing after schema validation
has run.

**Behavioral tightening.** Schema validation reports duplicate use of the same
object type for more than one operation root.

**Behavioral tightening.** Directive argument defaults are validated as part of
schema validation.

**Behavioral tightening.** The built-in `@deprecated(reason:)` argument is now
non-null. In v16 the argument type was nullable `String`, so a directive use
could explicitly pass `reason: null`. In v17, omit `reason` to use the default
deprecation reason; explicit `null` is no longer valid.

### Programmatic schema APIs

**New stable API.** v17 exposes public schema element types and assertions for
programmatic schema work, including `GraphQLField`, `GraphQLArgument`,
`GraphQLInputField`, `GraphQLEnumValue`, `assertField()`, `assertArgument()`,
`assertInputField()`, and `assertEnumValue()`.

**New stable API.** `GraphQLSchema.getField(parentType, fieldName)` resolves
ordinary fields and GraphQL meta fields such as `__typename`, `__schema`, and
`__type`.

**New stable API.** Custom `extensions` maps on schema elements support symbol
keys as well as string keys.

**New stable API.** `printDirective()` prints a directive definition without
printing an entire schema.

**Breaking change.** If you imported `GraphQLInterfaceTypeNormalizedConfig`
from a public entry point, replace that import with
`ReturnType<GraphQLInterfaceType['toConfig']>`.

## Language, AST, Parser, Printer, and Visitor APIs

### AST constants and visitors

**Breaking change.** The v16 alias types `KindEnum`, `TokenKindEnum`, and
`DirectiveLocationEnum` are gone. Use `Kind`, `TokenKind`, and
`DirectiveLocation`, which are const objects with matching union types.

```diff
- import type { KindEnum } from 'graphql';
+ import type { Kind } from 'graphql';
```

**Breaking change.** `getVisitFn()` is gone. Use
`getEnterLeaveForKind()` instead.

```js
const { enter, leave } = getEnterLeaveForKind(visitor, Kind.FIELD);
```

**Behavioral change.** Empty AST collections may be omitted as `undefined`.
Code that reads properties such as `arguments`, `directives`,
`variableDefinitions`, `interfaces`, `fields`, `types`, or `operationTypes`
should treat them as optional.

**New stable API.** `isSubscriptionOperationDefinitionNode()` narrows
subscription operation nodes.

### Fragment arguments

**Experimental or opt-in.** v17 adds proposal-backed fragment arguments behind
the `experimentalFragmentArguments` parser option. Older GraphQL.js versions
had an experimental `allowLegacyFragmentVariables` option, but that was
parser-only and did not work at runtime.

When enabled, fragment definitions can declare variables and fragment spreads
can pass arguments. See [Fragment Arguments](/docs/fragment-arguments).

## Validation

**Breaking change.** Passing a custom `TypeInfo` instance as a fifth argument
to `validate()` was removed. If you need a custom traversal with custom type
tracking, compose your visitor with `visitWithTypeInfo()`.

```diff
- const errors = validate(schema, document, rules, options, customTypeInfo);
+ const errors = validate(schema, document, rules, options);
```

**New stable API.** `hideSuggestions` removes "Did you mean ..." suggestion
text from diagnostics. This is useful for public APIs that do not want to leak
schema shape through error messages.

**New stable API.** `KnownOperationTypesRule` validates that an operation's
root type exists in the schema. For example, a `mutation` operation is invalid
when the schema has no mutation root type.

The defer/stream validation rules are documented with the experimental
incremental delivery feature rather than repeated here. See
[Defer and Stream](/docs/defer-stream).

## Utilities and Error Handling

### Schema Change utilities

**Deprecation.** `findBreakingChanges()` and `findDangerousChanges()` still
exist in v17 and are deprecated for removal in v18. Use
`findSchemaChanges()` for new schema registry and CI tooling.

**New stable API.** `findSchemaChanges()` reports breaking, dangerous, and safe
changes from one call. Code that switches to it should handle all three
categories; safe changes use the `SafeChangeType` and `SafeChange` shapes.

See [Schema Evolution](/docs/schema-evolution) for schema comparison examples.

### Removed helpers

**Breaking change.** The following deprecated helpers were removed:

- `assertValidName()` and `isValidNameError()`; use `assertName()`.
- `assertValidExecutionArguments()`; use `assertValidSchema()` for schema
  validation and `validateExecutionArgs()` for execution argument validation.
- `getOperationRootType()`; use `schema.getRootType(operation)`.
- `getFieldDefFn` from `TypeInfo`.
- `printError()` and `formatError()`; use `error.toString()` or
  `error.toJSON()`.

### `GraphQLError`

**Breaking change.** The positional `GraphQLError` constructor was removed.
Pass a message and an options object.

```diff
- new GraphQLError(message, nodes, source, positions, path, originalError);
+ new GraphQLError(message, {
+   nodes,
+   source,
+   positions,
+   path,
+   originalError,
+   extensions,
+ });
```

## Practical Migration Order

1. Update Node.js, TypeScript, and package-entry imports.
2. Compile and replace removed helpers, removed alias types, and positional
   `GraphQLError` calls.
3. Run `validateSchema()` and migrate invalid or ambiguous defaults to the new
   `default` model.
4. Update execution hosts: `execute()` versus
   `experimentalExecuteIncrementally()`, `createSourceEventStream()` validation,
   and subscription return types.
5. Migrate custom scalars to the v17 coercion method names while keeping the
   v16 names if you still support v16.
6. Adopt optional host features such as development mode, harnesses, abort
   signals, execution hooks, and fragment arguments only where they match your
   server design.

Run schema validation, operation validation, execution tests, and TypeScript
checks after each group. The upgrade is easiest to review when mechanical
changes are separated from behavior changes.