Repository: graphql/graphql-js
Pull request #3418 (2021-12-08 01:17:15)

[RFC] Client Controlled Nullability experiment implementation w/o execution
This branch is a subset of https://github.com/graphql/graphql-js/pull/3281 with a new flag introduced to mark the new operators experimental so they can be merged into master. 

To set up the experiment gates, inspiration was drawn from https://github.com/graphql/graphql-js/pull/1141

For additional context see https://github.com/graphql/graphql-js/pull/3281#issuecomment-987855321

State of the experiment:

We're merging in two parts of the original RFC and holding off on some of the more controversial bits that are still heavily in flux.

This PR only introduces parsing, lexing, printing, and visiting for the `?` and `!` operators and list syntax. It does not include validation or execution.

twof: Updates: I've merged in the list syntax, rebased on `main`, and I've got all tests passing. @IvanGoncharov let me know if there's anything I can do to help with the experiment flag implementation. 
github-actions[bot]: The latest changes of this PR are available on NPM as
[graphql@16.3.0-canary.pr.3418.64c65ba6a2e7e923ed9a476c945f3c72da7a6c7f](https://www.npmjs.com/package/graphql/v/16.3.0-canary.pr.3418.64c65ba6a2e7e923ed9a476c945f3c72da7a6c7f)
**Note: no gurantees provided so please use your own discretion.**

Also you can depend on latest version built from this PR:
`npm install --save graphql@canary-pr-3418`

github-actions[bot]: The latest changes of this PR are available on NPM as
[graphql@16.3.0-canary.pr.3418.662180be9091ee1edf699b4cf4922bd5b7705297](https://www.npmjs.com/package/graphql/v/16.3.0-canary.pr.3418.662180be9091ee1edf699b4cf4922bd5b7705297)
**Note: no gurantees provided so please use your own discretion.**

Also you can depend on latest version built from this PR:
`npm install --save graphql@canary-pr-3418`

twof: New behavior was selected for null propagation at the last WG meeting, there are going to be some pretty major changes incoming. We're sticking with the current list syntax.
twof: @saihaj are you or the other folks on the review team going to have time to take a look at it this week? If so, I can rebase on main and make sure it's up to date with the other branch with execution: https://github.com/graphql/graphql-js/pull/3510
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *compassionate-pike-271cb3* processing.


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 1d25a2e401ec099c4763b573d66ff3bbb09d054e |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/compassionate-pike-271cb3/deploys/62699bc31c060b000870578e |
twof: @IvanGoncharov Alright I've moved over the code that Apollo-iOS needs, as we discussed. Thanks again!

Patch:
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 5550c5a5..703fe227 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -348,7 +348,7 @@ rules:
   max-lines: off
   max-lines-per-function: off
   max-nested-callbacks: off
-  max-params: off
+  max-params: [error, 5] # TODO: drop to default number, which is 3
   max-statements: off
   max-statements-per-line: off
   multiline-comment-style: off
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8f014024..3e2d34fc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -157,7 +157,6 @@ jobs:
   build-npm-dist:
     name: Build 'npmDist' artifact
     runs-on: ubuntu-latest
-    needs: [test, fuzz, lint, integrationTests]
     steps:
       - name: Checkout repo
         uses: actions/checkout@v3
@@ -185,7 +184,6 @@ jobs:
   build-deno-dist:
     name: Build 'denoDist' artifact
     runs-on: ubuntu-latest
-    needs: [test, fuzz, lint, integrationTests]
     steps:
       - name: Checkout repo
         uses: actions/checkout@v3
diff --git a/benchmark/list-async-benchmark.js b/benchmark/list-async-benchmark.js
new file mode 100644
index 00000000..ed224835
--- /dev/null
+++ b/benchmark/list-async-benchmark.js
@@ -0,0 +1,26 @@
+import { execute } from 'graphql/execution/execute.js';
+import { parse } from 'graphql/language/parser.js';
+import { buildSchema } from 'graphql/utilities/buildASTSchema.js';
+
+const schema = buildSchema('type Query { listField: [String] }');
+const document = parse('{ listField }');
+
+function listField() {
+  const results = [];
+  for (let index = 0; index < 1000; index++) {
+    results.push(Promise.resolve(index));
+  }
+  return results;
+}
+
+export const benchmark = {
+  name: 'Execute Asynchronous List Field',
+  count: 10,
+  async measure() {
+    await execute({
+      schema,
+      document,
+      rootValue: { listField },
+    });
+  },
+};
diff --git a/benchmark/list-asyncIterable-benchmark.js b/benchmark/list-asyncIterable-benchmark.js
new file mode 100644
index 00000000..6b31ea5e
--- /dev/null
+++ b/benchmark/list-asyncIterable-benchmark.js
@@ -0,0 +1,24 @@
+import { execute } from 'graphql/execution/execute.js';
+import { parse } from 'graphql/language/parser.js';
+import { buildSchema } from 'graphql/utilities/buildASTSchema.js';
+
+const schema = buildSchema('type Query { listField: [String] }');
+const document = parse('{ listField }');
+
+async function* listField() {
+  for (let index = 0; index < 1000; index++) {
+    yield index;
+  }
+}
+
+export const benchmark = {
+  name: 'Execute Async Iterable List Field',
+  count: 10,
+  async measure() {
+    await execute({
+      schema,
+      document,
+      rootValue: { listField },
+    });
+  },
+};
diff --git a/benchmark/list-sync-benchmark.js b/benchmark/list-sync-benchmark.js
new file mode 100644
index 00000000..e797802a
--- /dev/null
+++ b/benchmark/list-sync-benchmark.js
@@ -0,0 +1,26 @@
+import { execute } from 'graphql/execution/execute.js';
+import { parse } from 'graphql/language/parser.js';
+import { buildSchema } from 'graphql/utilities/buildASTSchema.js';
+
+const schema = buildSchema('type Query { listField: [String] }');
+const document = parse('{ listField }');
+
+function listField() {
+  const results = [];
+  for (let index = 0; index < 1000; index++) {
+    results.push(index);
+  }
+  return results;
+}
+
+export const benchmark = {
+  name: 'Execute Synchronous List Field',
+  count: 10,
+  async measure() {
+    await execute({
+      schema,
+      document,
+      rootValue: { listField },
+    });
+  },
+};
diff --git a/resources/benchmark.ts b/resources/benchmark.ts
index 40cd33f3..0564f2a2 100644
--- a/resources/benchmark.ts
+++ b/resources/benchmark.ts
@@ -3,6 +3,7 @@ import * as cp from 'node:child_process';
 import * as fs from 'node:fs';
 import * as os from 'node:os';
 import * as path from 'node:path';
+import * as url from 'node:url';
 
 import { localRepoPath } from './utils';
 
@@ -95,7 +96,8 @@ function prepareBenchmarkProjects(
     const repoDir = path.join(tmpDir, hash);
     fs.rmSync(repoDir, { recursive: true, force: true });
     fs.mkdirSync(repoDir);
-    exec(`git archive "${hash}" | tar -xC "${repoDir}"`);
+    exec(`git clone --quiet "${localRepoPath()}" "${repoDir}"`);
+    exec(`git checkout --quiet --detach "${hash}"`, { cwd: repoDir });
     exec('npm --quiet ci --ignore-scripts', { cwd: repoDir });
     fs.renameSync(buildNPMArchive(repoDir), archivePath);
     fs.rmSync(repoDir, { recursive: true });
@@ -374,26 +376,29 @@ interface BenchmarkSample {
 }
 
 function sampleModule(modulePath: string): BenchmarkSample {
+  // To support Windows we need to use URL instead of path
+  const moduleURL = url.pathToFileURL(modulePath);
+
   const sampleCode = `
     import fs from 'node:fs';
 
-    import { benchmark } from '${modulePath}';
+    import { benchmark } from '${moduleURL}';
 
     // warm up, it looks like 7 is a magic number to reliably trigger JIT
-    benchmark.measure();
-    benchmark.measure();
-    benchmark.measure();
-    benchmark.measure();
-    benchmark.measure();
-    benchmark.measure();
-    benchmark.measure();
+    await benchmark.measure();
+    await benchmark.measure();
+    await benchmark.measure();
+    await benchmark.measure();
+    await benchmark.measure();
+    await benchmark.measure();
+    await benchmark.measure();
 
     const memBaseline = process.memoryUsage().heapUsed;
 
     const resourcesStart = process.resourceUsage();
     const startTime = process.hrtime.bigint();
     for (let i = 0; i < benchmark.count; ++i) {
-      benchmark.measure();
+      await benchmark.measure();
     }
     const timeDiff = Number(process.hrtime.bigint() - startTime);
     const resourcesEnd = process.resourceUsage();
diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts
index 83a78051..bd85f73d 100644
--- a/src/execution/collectFields.ts
+++ b/src/execution/collectFields.ts
@@ -86,6 +86,7 @@ export function collectSubfields(
   return subFieldNodes;
 }
 
+// eslint-disable-next-line max-params
 function collectFieldsImpl(
   schema: GraphQLSchema,
   fragments: ObjMap<FragmentDefinitionNode>,
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 6dc42461..cc3cbb9c 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -1,6 +1,6 @@
-import { devAssert } from '../jsutils/devAssert';
 import { inspect } from '../jsutils/inspect';
 import { invariant } from '../jsutils/invariant';
+import { isAsyncIterable } from '../jsutils/isAsyncIterable';
 import { isIterableObject } from '../jsutils/isIterableObject';
 import { isObjectLike } from '../jsutils/isObjectLike';
 import { isPromise } from '../jsutils/isPromise';
@@ -51,8 +51,13 @@ import {
   collectFields,
   collectSubfields as _collectSubfields,
 } from './collectFields';
+import { mapAsyncIterator } from './mapAsyncIterator';
 import { getArgumentValues, getVariableValues } from './values';
 
+/* eslint-disable max-params */
+// This file contains a lot of such errors but we plan to refactor it anyway
+// so just disable it for entire file.
+
 /**
  * A memoized collection of relevant subfields with regard to the return
  * type. Memoizing ensures the subfields are not repeatedly calculated, which
@@ -160,11 +165,6 @@ export interface ExecutionArgs {
  * a GraphQLError will be thrown immediately explaining the invalid input.
  */
 export function execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
-  const { schema, document, variableValues, rootValue } = args;
-
-  // If arguments are missing or incorrect, throw an error.
-  assertValidExecutionArguments(schema, document, variableValues);
-
   // If a valid execution context cannot be created due to incorrect arguments,
   // a "Response" with only errors is returned.
   const exeContext = buildExecutionContext(args);
@@ -174,6 +174,12 @@ export function execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
     return { errors: exeContext };
   }
 
+  return executeImpl(exeContext);
+}
+
+function executeImpl(
+  exeContext: ExecutionContext,
+): PromiseOrValue<ExecutionResult> {
   // Return a Promise that will eventually resolve to the data described by
   // The "Response" section of the GraphQL specification.
   //
@@ -186,8 +192,7 @@ export function execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
   // at which point we still log the error and null the parent field, which
   // in this case is the entire response.
   try {
-    const { operation } = exeContext;
-    const result = executeOperation(exeContext, operation, rootValue);
+    const result = executeOperation(exeContext);
     if (isPromise(result)) {
       return result.then(
         (data) => buildResponse(data, exeContext.errors),
@@ -231,35 +236,13 @@ function buildResponse(
   return errors.length === 0 ? { data } : { errors, data };
 }
 
-/**
- * Essential assertions before executing to provide developer feedback for
- * improper use of the GraphQL library.
- *
- * @internal
- */
-export function assertValidExecutionArguments(
-  schema: GraphQLSchema,
-  document: DocumentNode,
-  rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>,
-): void {
-  devAssert(document != null, 'Must provide document.');
-
-  // If the schema used for execution is invalid, throw an error.
-  assertValidSchema(schema);
-
-  // Variables, if provided, must be an object.
-  devAssert(
-    rawVariableValues == null || isObjectLike(rawVariableValues),
-    'Variables must be provided as an Object where each property is a variable value. Perhaps look to see if an unparsed JSON string was provided.',
-  );
-}
-
 /**
  * Constructs a ExecutionContext object from the arguments passed to
  * execute, which we will pass throughout the other execution methods.
  *
  * Throws a GraphQLError if a valid execution context cannot be created.
  *
+ * TODO: consider no longer exporting this function
  * @internal
  */
 export function buildExecutionContext(
@@ -277,6 +260,9 @@ export function buildExecutionContext(
     subscribeFieldResolver,
   } = args;
 
+  // If the schema used for execution is invalid, throw an error.
+  assertValidSchema(schema);
+
   let operation: OperationDefinitionNode | undefined;
   const fragments: ObjMap<FragmentDefinitionNode> = Object.create(null);
   for (const definition of document.definitions) {
@@ -339,15 +325,26 @@ export function buildExecutionContext(
   };
 }
 
+function buildPerEventExecutionContext(
+  exeContext: ExecutionContext,
+  payload: unknown,
+): ExecutionContext {
+  return {
+    ...exeContext,
+    rootValue: payload,
+    errors: [],
+  };
+}
+
 /**
  * Implements the "Executing operations" section of the spec.
  */
 function executeOperation(
   exeContext: ExecutionContext,
-  operation: OperationDefinitionNode,
-  rootValue: unknown,
-): PromiseOrValue<ObjMap<unknown> | null> {
-  const rootType = exeContext.schema.getRootType(operation.operation);
+): PromiseOrValue<ObjMap<unknown>> {
+  const { operation, schema, fragments, variableValues, rootValue } =
+    exeContext;
+  const rootType = schema.getRootType(operation.operation);
   if (rootType == null) {
     throw new GraphQLError(
       `Schema is not configured to execute ${operation.operation} operation.`,
@@ -356,9 +353,9 @@ function executeOperation(
   }
 
   const rootFields = collectFields(
-    exeContext.schema,
-    exeContext.fragments,
-    exeContext.variableValues,
+    schema,
+    fragments,
+    variableValues,
     rootType,
     operation.selectionSet,
   );
@@ -543,6 +540,7 @@ function executeField(
 }
 
 /**
+ * TODO: consider no longer exporting this function
  * @internal
  */
 export function buildResolveInfo(
@@ -693,6 +691,65 @@ function completeValue(
   );
 }
 
+/**
+ * Complete a async iterator value by completing the result and calling
+ * recursively until all the results are completed.
+ */
+async function completeAsyncIteratorValue(
+  exeContext: ExecutionContext,
+  itemType: GraphQLOutputType,
+  fieldNodes: ReadonlyArray<FieldNode>,
+  info: GraphQLResolveInfo,
+  path: Path,
+  iterator: AsyncIterator<unknown>,
+): Promise<ReadonlyArray<unknown>> {
+  let containsPromise = false;
+  const completedResults = [];
+  let index = 0;
+  // eslint-disable-next-line no-constant-condition
+  while (true) {
+    const fieldPath = addPath(path, index, undefined);
+    try {
+      // eslint-disable-next-line no-await-in-loop
+      const { value, done } = await iterator.next();
+      if (done) {
+        break;
+      }
+
+      try {
+        // TODO can the error checking logic be consolidated with completeListValue?
+        const completedItem = completeValue(
+          exeContext,
+          itemType,
+          fieldNodes,
+          info,
+          fieldPath,
+          value,
+        );
+        if (isPromise(completedItem)) {
+          containsPromise = true;
+        }
+        completedResults.push(completedItem);
+      } catch (rawError) {
+        completedResults.push(null);
+        const error = locatedError(
+          rawError,
+          fieldNodes,
+          pathToArray(fieldPath),
+        );
+        handleFieldError(error, itemType, exeContext);
+      }
+    } catch (rawError) {
+      completedResults.push(null);
+      const error = locatedError(rawError, fieldNodes, pathToArray(fieldPath));
+      handleFieldError(error, itemType, exeContext);
+      break;
+    }
+    index += 1;
+  }
+  return containsPromise ? Promise.all(completedResults) : completedResults;
+}
+
 /**
  * Complete a list value by completing each item in the list with the
  * inner type
@@ -705,6 +762,21 @@ function completeListValue(
   path: Path,
   result: unknown,
 ): PromiseOrValue<ReadonlyArray<unknown>> {
+  const itemType = returnType.ofType;
+
+  if (isAsyncIterable(result)) {
+    const iterator = result[Symbol.asyncIterator]();
+
+    return completeAsyncIteratorValue(
+      exeContext,
+      itemType,
+      fieldNodes,
+      info,
+      path,
+      iterator,
+    );
+  }
+
   if (!isIterableObject(result)) {
     throw new GraphQLError(
       `Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`,
@@ -713,7 +785,6 @@ function completeListValue(
 
   // This is specified as a simple map, however we're optimizing the path
   // where the list contains no Promises by avoiding creating another Promise.
-  const itemType = returnType.ofType;
   let containsPromise = false;
   const completedResults = Array.from(result, (item, index) => {
     // No need to modify the info object containing the path,
@@ -1009,3 +1080,215 @@ export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
       return property;
     }
   };
+
+/**
+ * Implements the "Subscribe" algorithm described in the GraphQL specification.
+ *
+ * Returns a Promise which resolves to either an AsyncIterator (if successful)
+ * or an ExecutionResult (error). The promise will be rejected if the schema or
+ * other arguments to this function are invalid, or if the resolved event stream
+ * is not an async iterable.
+ *
+ * If the client-provided arguments to this function do not result in a
+ * compliant subscription, a GraphQL Response (ExecutionResult) with
+ * descriptive errors and no data will be returned.
+ *
+ * If the source stream could not be created due to faulty subscription
+ * resolver logic or underlying systems, the promise will resolve to a single
+ * ExecutionResult containing `errors` and no `data`.
+ *
+ * If the operation succeeded, the promise resolves to an AsyncIterator, which
+ * yields a stream of ExecutionResults representing the response stream.
+ *
+ * Accepts either an object with named arguments, or individual arguments.
+ */
+export function subscribe(
+  args: ExecutionArgs,
+): PromiseOrValue<
+  AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
+> {
+  // If a valid execution context cannot be created due to incorrect arguments,
+  // a "Response" with only errors is returned.
+  const exeContext = buildExecutionContext(args);
+
+  // Return early errors if execution context failed.
+  if (!('schema' in exeContext)) {
+    return { errors: exeContext };
+  }
+
+  const resultOrStream = createSourceEventStreamImpl(exeContext);
+
+  if (isPromise(resultOrStream)) {
+    return resultOrStream.then((resolvedResultOrStream) =>
+      mapSourceToResponse(exeContext, resolvedResultOrStream),
+    );
+  }
+
+  return mapSourceToResponse(exeContext, resultOrStream);
+}
+
+function mapSourceToResponse(
+  exeContext: ExecutionContext,
+  resultOrStream: ExecutionResult | AsyncIterable<unknown>,
+): PromiseOrValue<
+  AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
+> {
+  if (!isAsyncIterable(resultOrStream)) {
+    return resultOrStream;
+  }
+
+  // For each payload yielded from a subscription, map it over the normal
+  // GraphQL `execute` function, with `payload` as the rootValue.
+  // This implements the "MapSourceToResponseEvent" algorithm described in
+  // the GraphQL specification. The `execute` function provides the
+  // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the
+  // "ExecuteQuery" algorithm, for which `execute` is also used.
+  return mapAsyncIterator(resultOrStream, (payload: unknown) =>
+    executeImpl(buildPerEventExecutionContext(exeContext, payload)),
+  );
+}
+
+/**
+ * Implements the "CreateSourceEventStream" algorithm described in the
+ * GraphQL specification, resolving the subscription source event stream.
+ *
+ * Returns a Promise which resolves to either an AsyncIterable (if successful)
+ * or an ExecutionResult (error). The promise will be rejected if the schema or
+ * other arguments to this function are invalid, or if the resolved event stream
+ * is not an async iterable.
+ *
+ * If the client-provided arguments to this function do not result in a
+ * compliant subscription, a GraphQL Response (ExecutionResult) with
+ * descriptive errors and no data will be returned.
+ *
+ * If the the source stream could not be created due to faulty subscription
+ * resolver logic or underlying systems, the promise will resolve to a single
+ * ExecutionResult containing `errors` and no `data`.
+ *
+ * If the operation succeeded, the promise resolves to the AsyncIterable for the
+ * event stream returned by the resolver.
+ *
+ * A Source Event Stream represents a sequence of events, each of which triggers
+ * a GraphQL execution for that event.
+ *
+ * This may be useful when hosting the stateful subscription service in a
+ * different process or machine than the stateless GraphQL execution engine,
+ * or otherwise separating these two steps. For more on this, see the
+ * "Supporting Subscriptions at Scale" information in the GraphQL specification.
+ */
+export function createSourceEventStream(
+  args: ExecutionArgs,
+): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult> {
+  // If a valid execution context cannot be created due to incorrect arguments,
+  // a "Response" with only errors is returned.
+  const exeContext = buildExecutionContext(args);
+
+  // Return early errors if execution context failed.
+  if (!('schema' in exeContext)) {
+    return { errors: exeContext };
+  }
+
+  return createSourceEventStreamImpl(exeContext);
+}
+
+function createSourceEventStreamImpl(
+  exeContext: ExecutionContext,
+): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult> {
+  try {
+    const eventStream = executeSubscription(exeContext);
+    if (isPromise(eventStream)) {
+      return eventStream.then(undefined, (error) => ({ errors: [error] }));
+    }
+
+    return eventStream;
+  } catch (error) {
+    return { errors: [error] };
+  }
+}
+
+function executeSubscription(
+  exeContext: ExecutionContext,
+): PromiseOrValue<AsyncIterable<unknown>> {
+  const { schema, fragments, operation, variableValues, rootValue } =
+    exeContext;
+
+  const rootType = schema.getSubscriptionType();
+  if (rootType == null) {
+    throw new GraphQLError(
+      'Schema is not configured to execute subscription operation.',
+      { nodes: operation },
+    );
+  }
+
+  const rootFields = collectFields(
+    schema,
+    fragments,
+    variableValues,
+    rootType,
+    operation.selectionSet,
+  );
+  const [responseName, fieldNodes] = [...rootFields.entries()][0];
+  const fieldName = fieldNodes[0].name.value;
+  const fieldDef = schema.getField(rootType, fieldName);
+
+  if (!fieldDef) {
+    throw new GraphQLError(
+      `The subscription field "${fieldName}" is not defined.`,
+      { nodes: fieldNodes },
+    );
+  }
+
+  const path = addPath(undefined, responseName, rootType.name);
+  const info = buildResolveInfo(
+    exeContext,
+    fieldDef,
+    fieldNodes,
+    rootType,
+    path,
+  );
+
+  try {
+    // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification.
+    // It differs from "ResolveFieldValue" due to providing a different `resolveFn`.
+
+    // Build a JS object of arguments from the field.arguments AST, using the
+    // variables scope to fulfill any variable references.
+    const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues);
+
+    // The resolve function's optional third argument is a context value that
+    // is provided to every resolve function within an execution. It is commonly
+    // used to represent an authenticated user, or request-specific caches.
+    const contextValue = exeContext.contextValue;
+
+    // Call the `subscribe()` resolver or the default resolver to produce an
+    // AsyncIterable yielding raw payloads.
+    const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver;
+    const result = resolveFn(rootValue, args, contextValue, info);
+
+    if (isPromise(result)) {
+      return result.then(assertEventStream).then(undefined, (error) => {
+        throw locatedError(error, fieldNodes, pathToArray(path));
+      });
+    }
+
+    return assertEventStream(result);
+  } catch (error) {
+    throw locatedError(error, fieldNodes, pathToArray(path));
+  }
+}
+
+function assertEventStream(result: unknown): AsyncIterable<unknown> {
+  if (result instanceof Error) {
+    throw result;
+  }
+
+  // Assert field returned an event stream, otherwise yield an error.
+  if (!isAsyncIterable(result)) {
+    throw new GraphQLError(
+      'Subscription field must return Async Iterable. ' +
+        `Received: ${inspect(result)}.`,
+    );
+  }
+
+  return result;
+}
diff --git a/src/execution/index.ts b/src/execution/index.ts
index 7727e6d5..b27a2c29 100644
--- a/src/execution/index.ts
+++ b/src/execution/index.ts
@@ -1,10 +1,12 @@
 export { pathToArray as responsePathAsArray } from '../jsutils/Path';
 
 export {
+  createSourceEventStream,
   execute,
   executeSync,
   defaultFieldResolver,
   defaultTypeResolver,
+  subscribe,
 } from './execute';
 
 export type {
@@ -13,8 +15,6 @@ export type {
   FormattedExecutionResult,
 } from './execute';
 
-export { subscribe, createSourceEventStream } from './subscribe';
-
 export {
   getArgumentValues,
   getVariableValues,
diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts
deleted file mode 100644
index 7a04480b..00000000
--- a/src/execution/subscribe.ts
+++ /dev/null
@@ -1,245 +0,0 @@
-import { inspect } from '../jsutils/inspect';
-import { isAsyncIterable } from '../jsutils/isAsyncIterable';
-import type { Maybe } from '../jsutils/Maybe';
-import { addPath, pathToArray } from '../jsutils/Path';
-
-import { GraphQLError } from '../error/GraphQLError';
-import { locatedError } from '../error/locatedError';
-
-import type { DocumentNode } from '../language/ast';
-
-import type { GraphQLFieldResolver } from '../type/definition';
-import type { GraphQLSchema } from '../type/schema';
-
-import { collectFields } from './collectFields';
-import type {
-  ExecutionArgs,
-  ExecutionContext,
-  ExecutionResult,
-} from './execute';
-import {
-  assertValidExecutionArguments,
-  buildExecutionContext,
-  buildResolveInfo,
-  execute,
-} from './execute';
-import { mapAsyncIterator } from './mapAsyncIterator';
-import { getArgumentValues } from './values';
-
-/**
- * Implements the "Subscribe" algorithm described in the GraphQL specification.
- *
- * Returns a Promise which resolves to either an AsyncIterator (if successful)
- * or an ExecutionResult (error). The promise will be rejected if the schema or
- * other arguments to this function are invalid, or if the resolved event stream
- * is not an async iterable.
- *
- * If the client-provided arguments to this function do not result in a
- * compliant subscription, a GraphQL Response (ExecutionResult) with
- * descriptive errors and no data will be returned.
- *
- * If the source stream could not be created due to faulty subscription
- * resolver logic or underlying systems, the promise will resolve to a single
- * ExecutionResult containing `errors` and no `data`.
- *
- * If the operation succeeded, the promise resolves to an AsyncIterator, which
- * yields a stream of ExecutionResults representing the response stream.
- *
- * Accepts either an object with named arguments, or individual arguments.
- */
-export async function subscribe(
-  args: ExecutionArgs,
-): Promise<AsyncGenerator<ExecutionResult, void, void> | ExecutionResult> {
-  const {
-    schema,
-    document,
-    rootValue,
-    contextValue,
-    variableValues,
-    operationName,
-    fieldResolver,
-    subscribeFieldResolver,
-  } = args;
-
-  const resultOrStream = await createSourceEventStream(
-    schema,
-    document,
-    rootValue,
-    contextValue,
-    variableValues,
-    operationName,
-    subscribeFieldResolver,
-  );
-
-  if (!isAsyncIterable(resultOrStream)) {
-    return resultOrStream;
-  }
-
-  // For each payload yielded from a subscription, map it over the normal
-  // GraphQL `execute` function, with `payload` as the rootValue.
-  // This implements the "MapSourceToResponseEvent" algorithm described in
-  // the GraphQL specification. The `execute` function provides the
-  // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the
-  // "ExecuteQuery" algorithm, for which `execute` is also used.
-  const mapSourceToResponse = (payload: unknown) =>
-    execute({
-      schema,
-      document,
-      rootValue: payload,
-      contextValue,
-      variableValues,
-      operationName,
-      fieldResolver,
-    });
-
-  // Map every source value to a ExecutionResult value as described above.
-  return mapAsyncIterator(resultOrStream, mapSourceToResponse);
-}
-
-/**
- * Implements the "CreateSourceEventStream" algorithm described in the
- * GraphQL specification, resolving the subscription source event stream.
- *
- * Returns a Promise which resolves to either an AsyncIterable (if successful)
- * or an ExecutionResult (error). The promise will be rejected if the schema or
- * other arguments to this function are invalid, or if the resolved event stream
- * is not an async iterable.
- *
- * If the client-provided arguments to this function do not result in a
- * compliant subscription, a GraphQL Response (ExecutionResult) with
- * descriptive errors and no data will be returned.
- *
- * If the the source stream could not be created due to faulty subscription
- * resolver logic or underlying systems, the promise will resolve to a single
- * ExecutionResult containing `errors` and no `data`.
- *
- * If the operation succeeded, the promise resolves to the AsyncIterable for the
- * event stream returned by the resolver.
- *
- * A Source Event Stream represents a sequence of events, each of which triggers
- * a GraphQL execution for that event.
- *
- * This may be useful when hosting the stateful subscription service in a
- * different process or machine than the stateless GraphQL execution engine,
- * or otherwise separating these two steps. For more on this, see the
- * "Supporting Subscriptions at Scale" information in the GraphQL specification.
- */
-export async function createSourceEventStream(
-  schema: GraphQLSchema,
-  document: DocumentNode,
-  rootValue?: unknown,
-  contextValue?: unknown,
-  variableValues?: Maybe<{ readonly [variable: string]: unknown }>,
-  operationName?: Maybe<string>,
-  subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
-): Promise<AsyncIterable<unknown> | ExecutionResult> {
-  // If arguments are missing or incorrectly typed, this is an internal
-  // developer mistake which should throw an early error.
-  assertValidExecutionArguments(schema, document, variableValues);
-
-  // If a valid execution context cannot be created due to incorrect arguments,
-  // a "Response" with only errors is returned.
-  const exeContext = buildExecutionContext({
-    schema,
-    document,
-    rootValue,
-    contextValue,
-    variableValues,
-    operationName,
-    subscribeFieldResolver,
-  });
-
-  // Return early errors if execution context failed.
-  if (!('schema' in exeContext)) {
-    return { errors: exeContext };
-  }
-
-  try {
-    const eventStream = await executeSubscription(exeContext);
-
-    // Assert field returned an event stream, otherwise yield an error.
-    if (!isAsyncIterable(eventStream)) {
-      throw new Error(
-        'Subscription field must return Async Iterable. ' +
-          `Received: ${inspect(eventStream)}.`,
-      );
-    }
-
-    return eventStream;
-  } catch (error) {
-    // If it GraphQLError, report it as an ExecutionResult, containing only errors and no data.
-    // Otherwise treat the error as a system-class error and re-throw it.
-    if (error instanceof GraphQLError) {
-      return { errors: [error] };
-    }
-    throw error;
-  }
-}
-
-async function executeSubscription(
-  exeContext: ExecutionContext,
-): Promise<unknown> {
-  const { schema, fragments, operation, variableValues, rootValue } =
-    exeContext;
-
-  const rootType = schema.getSubscriptionType();
-  if (rootType == null) {
-    throw new GraphQLError(
-      'Schema is not configured to execute subscription operation.',
-      { nodes: operation },
-    );
-  }
-
-  const rootFields = collectFields(
-    schema,
-    fragments,
-    variableValues,
-    rootType,
-    operation.selectionSet,
-  );
-  const [responseName, fieldNodes] = [...rootFields.entries()][0];
-  const fieldName = fieldNodes[0].name.value;
-  const fieldDef = schema.getField(rootType, fieldName);
-
-  if (!fieldDef) {
-    throw new GraphQLError(
-      `The subscription field "${fieldName}" is not defined.`,
-      { nodes: fieldNodes },
-    );
-  }
-
-  const path = addPath(undefined, responseName, rootType.name);
-  const info = buildResolveInfo(
-    exeContext,
-    fieldDef,
-    fieldNodes,
-    rootType,
-    path,
-  );
-
-  try {
-    // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification.
-    // It differs from "ResolveFieldValue" due to providing a different `resolveFn`.
-
-    // Build a JS object of arguments from the field.arguments AST, using the
-    // variables scope to fulfill any variable references.
-    const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues);
-
-    // The resolve function's optional third argument is a context value that
-    // is provided to every resolve function within an execution. It is commonly
-    // used to represent an authenticated user, or request-specific caches.
-    const contextValue = exeContext.contextValue;
-
-    // Call the `subscribe()` resolver or the default resolver to produce an
-    // AsyncIterable yielding raw payloads.
-    const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver;
-    const eventStream = await resolveFn(rootValue, args, contextValue, info);
-
-    if (eventStream instanceof Error) {
-      throw eventStream;
-    }
-    return eventStream;
-  } catch (error) {
-    throw locatedError(error, fieldNodes, pathToArray(path));
-  }
-}
diff --git a/src/index.ts b/src/index.ts
index bce254f8..b3a06a18 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -229,6 +229,7 @@ export {
   isDefinitionNode,
   isExecutableDefinitionNode,
   isSelectionNode,
+  isNullabilityAssertionNode,
   isValueNode,
   isConstValueNode,
   isTypeNode,
@@ -260,6 +261,10 @@ export type {
   SelectionNode,
   FieldNode,
   ArgumentNode,
+  NullabilityAssertionNode,
+  NonNullAssertionNode,
+  ErrorBoundaryNode,
+  ListNullabilityOperatorNode,
   ConstArgumentNode,
   FragmentSpreadNode,
   InlineFragmentNode,
diff --git a/src/language/ast.ts b/src/language/ast.ts
index 0b30366d..8faffaec 100644
--- a/src/language/ast.ts
+++ b/src/language/ast.ts
@@ -95,6 +95,7 @@ export class Token {
   readonly prev: Token | null;
   readonly next: Token | null;
 
+  // eslint-disable-next-line max-params
   constructor(
     kind: TokenKind,
     start: number,
@@ -179,7 +180,10 @@ export type ASTNode =
   | InterfaceTypeExtensionNode
   | UnionTypeExtensionNode
   | EnumTypeExtensionNode
-  | InputObjectTypeExtensionNode;
+  | InputObjectTypeExtensionNode
+  | NonNullAssertionNode
+  | ErrorBoundaryNode
+  | ListNullabilityOperatorNode;
 
 /**
  * Utility type listing all nodes indexed by their kind.
@@ -206,8 +210,22 @@ export const QueryDocumentKeys: {
   VariableDefinition: ['variable', 'type', 'defaultValue', 'directives'],
   Variable: ['name'],
   SelectionSet: ['selections'],
-  Field: ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
+  Field: [
+    'alias',
+    'name',
+    'arguments',
+    'directives',
+    'selectionSet',
+    // Note: Client Controlled Nullability is experimental and may be changed
+    // or removed in the future.
+    'nullabilityAssertion',
+  ],
   Argument: ['name', 'value'],
+  // Note: Client Controlled Nullability is experimental and may be changed
+  // or removed in the future.
+  ListNullabilityOperator: ['nullabilityAssertion'],
+  NonNullAssertion: ['nullabilityAssertion'],
+  ErrorBoundary: ['nullabilityAssertion'],
 
   FragmentSpread: ['name', 'directives'],
   InlineFragment: ['typeCondition', 'directives', 'selectionSet'],
@@ -358,10 +376,36 @@ export interface FieldNode {
   readonly alias?: NameNode;
   readonly name: NameNode;
   readonly arguments?: ReadonlyArray<ArgumentNode>;
+  // Note: Client Controlled Nullability is experimental
+  // and may be changed or removed in the future.
+  readonly nullabilityAssertion?: NullabilityAssertionNode;
   readonly directives?: ReadonlyArray<DirectiveNode>;
   readonly selectionSet?: SelectionSetNode;
 }
 
+export type NullabilityAssertionNode =
+  | NonNullAssertionNode
+  | ErrorBoundaryNode
+  | ListNullabilityOperatorNode;
+
+export interface ListNullabilityOperatorNode {
+  readonly kind: Kind.LIST_NULLABILITY_OPERATOR;
+  readonly loc?: Location;
+  readonly nullabilityAssertion?: NullabilityAssertionNode;
+}
+
+export interface NonNullAssertionNode {
+  readonly kind: Kind.NON_NULL_ASSERTION;
+  readonly loc?: Location;
+  readonly nullabilityAssertion?: ListNullabilityOperatorNode;
+}
+
+export interface ErrorBoundaryNode {
+  readonly kind: Kind.ERROR_BOUNDARY;
+  readonly loc?: Location;
+  readonly nullabilityAssertion?: ListNullabilityOperatorNode;
+}
+
 export interface ArgumentNode {
   readonly kind: Kind.ARGUMENT;
   readonly loc?: Location;
diff --git a/src/language/index.ts b/src/language/index.ts
index ad8e082c..0cb82194 100644
--- a/src/language/index.ts
+++ b/src/language/index.ts
@@ -34,6 +34,10 @@ export type {
   SelectionSetNode,
   SelectionNode,
   FieldNode,
+  NullabilityAssertionNode,
+  NonNullAssertionNode,
+  ErrorBoundaryNode,
+  ListNullabilityOperatorNode,
   ArgumentNode,
   ConstArgumentNode,
   FragmentSpreadNode,
@@ -88,6 +92,7 @@ export {
   isDefinitionNode,
   isExecutableDefinitionNode,
   isSelectionNode,
+  isNullabilityAssertionNode,
   isValueNode,
   isConstValueNode,
   isTypeNode,
diff --git a/src/language/kinds.ts b/src/language/kinds.ts
index ccbca959..d606c12c 100644
--- a/src/language/kinds.ts
+++ b/src/language/kinds.ts
@@ -13,6 +13,11 @@ export enum Kind {
   FIELD = 'Field',
   ARGUMENT = 'Argument',
 
+  /** Nullability Modifiers */
+  LIST_NULLABILITY_OPERATOR = 'ListNullabilityOperator',
+  NON_NULL_ASSERTION = 'NonNullAssertion',
+  ERROR_BOUNDARY = 'ErrorBoundary',
+
   /** Fragments */
   FRAGMENT_SPREAD = 'FragmentSpread',
   INLINE_FRAGMENT = 'InlineFragment',
diff --git a/src/language/lexer.ts b/src/language/lexer.ts
index 818f81b2..c3dc9322 100644
--- a/src/language/lexer.ts
+++ b/src/language/lexer.ts
@@ -91,6 +91,7 @@ export class Lexer {
 export function isPunctuatorTokenKind(kind: TokenKind): boolean {
   return (
     kind === TokenKind.BANG ||
+    kind === TokenKind.QUESTION_MARK ||
     kind === TokenKind.DOLLAR ||
     kind === TokenKind.AMP ||
     kind === TokenKind.PAREN_L ||
@@ -281,6 +282,13 @@ function readNextToken(lexer: Lexer, start: number): Token {
         return createToken(lexer, TokenKind.PIPE, position, position + 1);
       case 0x007d: // }
         return createToken(lexer, TokenKind.BRACE_R, position, position + 1);
+      case 0x003f: // ?
+        return createToken(
+          lexer,
+          TokenKind.QUESTION_MARK,
+          position,
+          position + 1,
+        );
       // StringValue
       case 0x0022: // "
         if (
diff --git a/src/language/parser.ts b/src/language/parser.ts
index 282ee168..e4bd5e87 100644
--- a/src/language/parser.ts
+++ b/src/language/parser.ts
@@ -20,6 +20,7 @@ import type {
   EnumTypeExtensionNode,
   EnumValueDefinitionNode,
   EnumValueNode,
+  ErrorBoundaryNode,
   FieldDefinitionNode,
   FieldNode,
   FloatValueNode,
@@ -32,11 +33,14 @@ import type {
   InterfaceTypeDefinitionNode,
   InterfaceTypeExtensionNode,
   IntValueNode,
+  ListNullabilityOperatorNode,
   ListTypeNode,
   ListValueNode,
   NamedTypeNode,
   NameNode,
+  NonNullAssertionNode,
   NonNullTypeNode,
+  NullabilityAssertionNode,
   NullValueNode,
   ObjectFieldNode,
   ObjectTypeDefinitionNode,
@@ -94,6 +98,29 @@ export interface ParseOptions {
    * ```
    */
   allowLegacyFragmentVariables?: boolean;
+
+  /**
+   * EXPERIMENTAL:
+   *
+   * If enabled, the parser will understand and parse Client Controlled Nullability
+   * Designators contained in Fields. They'll be represented in the
+   * `nullabilityAssertion` field of the FieldNode.
+   *
+   * The syntax looks like the following:
+   *
+   * ```graphql
+   *   {
+   *     nullableField!
+   *     nonNullableField?
+   *     nonNullableSelectionSet? {
+   *       childField!
+   *     }
+   *   }
+   * ```
+   * Note: this feature is experimental and may change or be removed in the
+   * future.
+   */
+  experimentalClientControlledNullability?: boolean;
 }
 
 /**
@@ -431,6 +458,9 @@ export class Parser {
       alias,
       name,
       arguments: this.parseArguments(false),
+      // Experimental support for Client Controlled Nullability changes
+      // the grammar of Field:
+      nullabilityAssertion: this.parseNullabilityAssertion(),
       directives: this.parseDirectives(false),
       selectionSet: this.peek(TokenKind.BRACE_L)
         ? this.parseSelectionSet()
@@ -438,6 +468,41 @@ export class Parser {
     });
   }
 
+  // TODO: add grammar comment after it finalizes
+  parseNullabilityAssertion(): NullabilityAssertionNode | undefined {
+    // Note: Client Controlled Nullability is experimental and may be changed or
+    // removed in the future.
+    if (this._options?.experimentalClientControlledNullability !== true) {
+      return undefined;
+    }
+
+    const start = this._lexer.token;
+    let nullabilityAssertion;
+
+    if (this.expectOptionalToken(TokenKind.BRACKET_L)) {
+      const innerModifier = this.parseNullabilityAssertion();
+      this.expectToken(TokenKind.BRACKET_R);
+      nullabilityAssertion = this.node<ListNullabilityOperatorNode>(start, {
+        kind: Kind.LIST_NULLABILITY_OPERATOR,
+        nullabilityAssertion: innerModifier,
+      });
+    }
+
+    if (this.expectOptionalToken(TokenKind.BANG)) {
+      nullabilityAssertion = this.node<NonNullAssertionNode>(start, {
+        kind: Kind.NON_NULL_ASSERTION,
+        nullabilityAssertion,
+      });
+    } else if (this.expectOptionalToken(TokenKind.QUESTION_MARK)) {
+      nullabilityAssertion = this.node<ErrorBoundaryNode>(start, {
+        kind: Kind.ERROR_BOUNDARY,
+        nullabilityAssertion,
+      });
+    }
+
+    return nullabilityAssertion;
+  }
+
   /**
    * Arguments[Const] : ( Argument[?Const]+ )
    */
diff --git a/src/language/predicates.ts b/src/language/predicates.ts
index a390f4ee..8a96681e 100644
--- a/src/language/predicates.ts
+++ b/src/language/predicates.ts
@@ -3,6 +3,7 @@ import type {
   ConstValueNode,
   DefinitionNode,
   ExecutableDefinitionNode,
+  NullabilityAssertionNode,
   SelectionNode,
   TypeDefinitionNode,
   TypeExtensionNode,
@@ -38,6 +39,16 @@ export function isSelectionNode(node: ASTNode): node is SelectionNode {
   );
 }
 
+export function isNullabilityAssertionNode(
+  node: ASTNode,
+): node is NullabilityAssertionNode {
+  return (
+    node.kind === Kind.LIST_NULLABILITY_OPERATOR ||
+    node.kind === Kind.NON_NULL_ASSERTION ||
+    node.kind === Kind.ERROR_BOUNDARY
+  );
+}
+
 export function isValueNode(node: ASTNode): node is ValueNode {
   return (
     node.kind === Kind.VARIABLE ||
diff --git a/src/language/printer.ts b/src/language/printer.ts
index 38cb2544..127e6cc4 100644
--- a/src/language/printer.ts
+++ b/src/language/printer.ts
@@ -55,20 +55,53 @@ const printDocASTReducer: ASTReducer<string> = {
   SelectionSet: { leave: ({ selections }) => block(selections) },
 
   Field: {
-    leave({ alias, name, arguments: args, directives, selectionSet }) {
-      const prefix = wrap('', alias, ': ') + name;
+    leave({
+      alias,
+      name,
+      arguments: args,
+      nullabilityAssertion,
+      directives,
+      selectionSet,
+    }) {
+      const prefix = join([wrap('', alias, ': '), name], '');
       let argsLine = prefix + wrap('(', join(args, ', '), ')');
 
       if (argsLine.length > MAX_LINE_LENGTH) {
         argsLine = prefix + wrap('(\n', indent(join(args, '\n')), '\n)');
       }
 
-      return join([argsLine, join(directives, ' '), selectionSet], ' ');
+      return join([
+        argsLine,
+        // Note: Client Controlled Nullability is experimental and may be
+        // changed or removed in the future.
+        nullabilityAssertion,
+        wrap(' ', join(directives, ' ')),
+        wrap(' ', selectionSet),
+      ]);
     },
   },
-
   Argument: { leave: ({ name, value }) => name + ': ' + value },
 
+  // Nullability Modifiers
+
+  ListNullabilityOperator: {
+    leave({ nullabilityAssertion }) {
+      return join(['[', nullabilityAssertion, ']']);
+    },
+  },
+
+  NonNullAssertion: {
+    leave({ nullabilityAssertion }) {
+      return join([nullabilityAssertion, '!']);
+    },
+  },
+
+  ErrorBoundary: {
+    leave({ nullabilityAssertion }) {
+      return join([nullabilityAssertion, '?']);
+    },
+  },
+
   // Fragments
 
   FragmentSpread: {
diff --git a/src/language/source.ts b/src/language/source.ts
index 15f65fce..49c15679 100644
--- a/src/language/source.ts
+++ b/src/language/source.ts
@@ -1,5 +1,4 @@
 import { devAssert } from '../jsutils/devAssert';
-import { inspect } from '../jsutils/inspect';
 import { instanceOf } from '../jsutils/instanceOf';
 
 interface Location {
@@ -24,11 +23,6 @@ export class Source {
     name: string = 'GraphQL request',
     locationOffset: Location = { line: 1, column: 1 },
   ) {
-    devAssert(
-      typeof body === 'string',
-      `Body must be a string. Received: ${inspect(body)}.`,
-    );
-
     this.body = body;
     this.name = name;
     this.locationOffset = locationOffset;
diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts
index e1fec5a7..1bba933d 100644
--- a/src/language/tokenKind.ts
+++ b/src/language/tokenKind.ts
@@ -6,6 +6,7 @@ export enum TokenKind {
   SOF = '<SOF>',
   EOF = '<EOF>',
   BANG = '!',
+  QUESTION_MARK = '?',
   DOLLAR = '$',
   AMP = '&',
   PAREN_L = '(',
diff --git a/src/type/assertName.ts b/src/type/assertName.ts
index f4f96fda..b88a6bf3 100644
--- a/src/type/assertName.ts
+++ b/src/type/assertName.ts
@@ -1,5 +1,3 @@
-import { devAssert } from '../jsutils/devAssert';
-
 import { GraphQLError } from '../error/GraphQLError';
 
 import { isNameContinue, isNameStart } from '../language/characterClasses';
@@ -8,9 +6,6 @@ import { isNameContinue, isNameStart } from '../language/characterClasses';
  * Upholds the spec rules about naming.
  */
 export function assertName(name: string): string {
-  devAssert(name != null, 'Must provide name.');
-  devAssert(typeof name === 'string', 'Expected name to be a string.');
-
   if (name.length === 0) {
     throw new GraphQLError('Expected name to be a non-empty string.');
   }
diff --git a/src/type/definition.ts b/src/type/definition.ts
index 9eea02e8..f4cb1a90 100644
--- a/src/type/definition.ts
+++ b/src/type/definition.ts
@@ -3,7 +3,6 @@ import { didYouMean } from '../jsutils/didYouMean';
 import { identityFunc } from '../jsutils/identityFunc';
 import { inspect } from '../jsutils/inspect';
 import { instanceOf } from '../jsutils/instanceOf';
-import { isObjectLike } from '../jsutils/isObjectLike';
 import { keyMap } from '../jsutils/keyMap';
 import { keyValMap } from '../jsutils/keyValMap';
 import { mapValue } from '../jsutils/mapValue';
@@ -50,10 +49,7 @@ import type { GraphQLSchema } from './schema';
 /**
  * These are all of the possible kinds of types.
  */
-export type GraphQLType =
-  | GraphQLNamedType
-  | GraphQLList<GraphQLType>
-  | GraphQLNonNull<GraphQLNullableType>;
+export type GraphQLType = GraphQLNamedType | GraphQLWrappingType;
 
 export function isType(type: unknown): type is GraphQLType {
   return (
@@ -183,7 +179,9 @@ export function isNonNullType(
   return instanceOf(type, GraphQLNonNull);
 }
 
-export function assertNonNullType(type: unknown): GraphQLNonNull<GraphQLType> {
+export function assertNonNullType(
+  type: unknown,
+): GraphQLNonNull<GraphQLNullableType> {
   if (!isNonNullType(type)) {
     throw new Error(`Expected ${inspect(type)} to be a GraphQL Non-Null type.`);
   }
@@ -322,11 +320,6 @@ export class GraphQLList<T extends GraphQLType> {
   readonly ofType: T;
 
   constructor(ofType: T) {
-    devAssert(
-      isType(ofType),
-      `Expected ${inspect(ofType)} to be a GraphQL type.`,
-    );
-
     this.ofType = ofType;
   }
 
@@ -368,11 +361,6 @@ export class GraphQLNonNull<T extends GraphQLNullableType> {
   readonly ofType: T;
 
   constructor(ofType: T) {
-    devAssert(
-      isNullableType(ofType),
-      `Expected ${inspect(ofType)} to be a GraphQL nullable type.`,
-    );
-
     this.ofType = ofType;
   }
 
@@ -395,7 +383,7 @@ export class GraphQLNonNull<T extends GraphQLNullableType> {
 
 export type GraphQLWrappingType =
   | GraphQLList<GraphQLType>
-  | GraphQLNonNull<GraphQLType>;
+  | GraphQLNonNull<GraphQLNullableType>;
 
 export function isWrappingType(type: unknown): type is GraphQLWrappingType {
   return isListType(type) || isNonNullType(type);
@@ -583,18 +571,6 @@ export class GraphQLScalarType<TInternal = unknown, TExternal = TInternal> {
     this.astNode = config.astNode;
     this.extensionASTNodes = config.extensionASTNodes ?? [];
 
-    devAssert(
-      config.specifiedByURL == null ||
-        typeof config.specifiedByURL === 'string',
-      `${this.name} must provide "specifiedByURL" as a string, ` +
-        `but got: ${inspect(config.specifiedByURL)}.`,
-    );
-
-    devAssert(
-      config.serialize == null || typeof config.serialize === 'function',
-      `${this.name} must provide "serialize" function. If this custom Scalar is also used as an input type, ensure "parseValue" and "parseLiteral" functions are also provided.`,
-    );
-
     if (config.parseLiteral) {
       devAssert(
         typeof config.parseValue === 'function' &&
@@ -745,11 +721,6 @@ export class GraphQLObjectType<TSource = any, TContext = any> {
 
     this._fields = () => defineFieldMap(config);
     this._interfaces = () => defineInterfaces(config);
-    devAssert(
-      config.isTypeOf == null || typeof config.isTypeOf === 'function',
-      `${this.name} must provide "isTypeOf" as a function, ` +
-        `but got: ${inspect(config.isTypeOf)}.`,
-    );
   }
 
   get [Symbol.toStringTag]() {
@@ -797,12 +768,7 @@ function defineInterfaces(
     GraphQLObjectTypeConfig<any, any> | GraphQLInterfaceTypeConfig<any, any>
   >,
 ): ReadonlyArray<GraphQLInterfaceType> {
-  const interfaces = resolveReadonlyArrayThunk(config.interfaces ?? []);
-  devAssert(
-    Array.isArray(interfaces),
-    `${config.name} interfaces must be an Array or a function which returns an Array.`,
-  );
-  return interfaces;
+  return resolveReadonlyArrayThunk(config.interfaces ?? []);
 }
 
 function defineFieldMap<TSource, TContext>(
@@ -812,28 +778,9 @@ function defineFieldMap<TSource, TContext>(
   >,
 ): GraphQLFieldMap<TSource, TContext> {
   const fieldMap = resolveObjMapThunk(config.fields);
-  devAssert(
-    isPlainObj(fieldMap),
-    `${config.name} fields must be an object with field names as keys or a function which returns such an object.`,
-  );
 
   return mapValue(fieldMap, (fieldConfig, fieldName) => {
-    devAssert(
-      isPlainObj(fieldConfig),
-      `${config.name}.${fieldName} field config must be an object.`,
-    );
-    devAssert(
-      fieldConfig.resolve == null || typeof fieldConfig.resolve === 'function',
-      `${config.name}.${fieldName} field resolver must be a function if ` +
-        `provided, but got: ${inspect(fieldConfig.resolve)}.`,
-    );
-
     const argsConfig = fieldConfig.args ?? {};
-    devAssert(
-      isPlainObj(argsConfig),
-      `${config.name}.${fieldName} args must be an object with argument names as keys.`,
-    );
-
     return {
       name: assertName(fieldName),
       description: fieldConfig.description,
@@ -862,10 +809,6 @@ export function defineArguments(
   }));
 }
 
-function isPlainObj(obj: unknown): boolean {
-  return isObjectLike(obj) && !Array.isArray(obj);
-}
-
 function fieldsToFieldsConfig<TSource, TContext>(
   fields: GraphQLFieldMap<TSource, TContext>,
 ): GraphQLFieldConfigMap<TSource, TContext> {
@@ -1098,11 +1041,6 @@ export class GraphQLInterfaceType {
 
     this._fields = defineFieldMap.bind(undefined, config);
     this._interfaces = defineInterfaces.bind(undefined, config);
-    devAssert(
-      config.resolveType == null || typeof config.resolveType === 'function',
-      `${this.name} must provide "resolveType" as a function, ` +
-        `but got: ${inspect(config.resolveType)}.`,
-    );
   }
 
   get [Symbol.toStringTag]() {
@@ -1225,11 +1163,6 @@ export class GraphQLUnionType {
     this.extensionASTNodes = config.extensionASTNodes ?? [];
 
     this._types = defineTypes.bind(undefined, config);
-    devAssert(
-      config.resolveType == null || typeof config.resolveType === 'function',
-      `${this.name} must provide "resolveType" as a function, ` +
-        `but got: ${inspect(config.resolveType)}.`,
-    );
   }
 
   get [Symbol.toStringTag]() {
@@ -1267,12 +1200,7 @@ export class GraphQLUnionType {
 function defineTypes(
   config: Readonly<GraphQLUnionTypeConfig<unknown, unknown>>,
 ): ReadonlyArray<GraphQLObjectType> {
-  const types = resolveReadonlyArrayThunk(config.types);
-  devAssert(
-    Array.isArray(types),
-    `Must provide Array of types or a function which returns such an array for Union ${config.name}.`,
-  );
-  return types;
+  return resolveReadonlyArrayThunk(config.types);
 }
 
 export interface GraphQLUnionTypeConfig<TSource, TContext> {
@@ -1351,7 +1279,16 @@ export class GraphQLEnumType /* <T> */ {
     this.astNode = config.astNode;
     this.extensionASTNodes = config.extensionASTNodes ?? [];
 
-    this._values = defineEnumValues(this.name, config.values);
+    this._values = Object.entries(config.values).map(
+      ([valueName, valueConfig]) => ({
+        name: assertEnumValueName(valueName),
+        description: valueConfig.description,
+        value: valueConfig.value !== undefined ? valueConfig.value : valueName,
+        deprecationReason: valueConfig.deprecationReason,
+        extensions: toObjMap(valueConfig.extensions),
+        astNode: valueConfig.astNode,
+      }),
+    );
     this._valueLookup = new Map(
       this._values.map((enumValue) => [enumValue.value, enumValue]),
     );
@@ -1467,31 +1404,6 @@ function didYouMeanEnumValue(
   return didYouMean('the enum value', suggestedValues);
 }
 
-function defineEnumValues(
-  typeName: string,
-  valueMap: GraphQLEnumValueConfigMap /* <T> */,
-): ReadonlyArray<GraphQLEnumValue /* <T> */> {
-  devAssert(
-    isPlainObj(valueMap),
-    `${typeName} values must be an object with value names as keys.`,
-  );
-  return Object.entries(valueMap).map(([valueName, valueConfig]) => {
-    devAssert(
-      isPlainObj(valueConfig),
-      `${typeName}.${valueName} must refer to an object with a "value" key ` +
-        `representing an internal value but got: ${inspect(valueConfig)}.`,
-    );
-    return {
-      name: assertEnumValueName(valueName),
-      description: valueConfig.description,
-      value: valueConfig.value !== undefined ? valueConfig.value : valueName,
-      deprecationReason: valueConfig.deprecationReason,
-      extensions: toObjMap(valueConfig.extensions),
-      astNode: valueConfig.astNode,
-    };
-  });
-}
-
 export interface GraphQLEnumTypeConfig {
   name: string;
   description?: Maybe<string>;
@@ -1636,10 +1548,6 @@ function defineInputFieldMap(
   config: Readonly<GraphQLInputObjectTypeConfig>,
 ): GraphQLInputFieldMap {
   const fieldMap = resolveObjMapThunk(config.fields);
-  devAssert(
-    isPlainObj(fieldMap),
-    `${config.name} fields must be an object with field names as keys or a function which returns such an object.`,
-  );
   return mapValue(fieldMap, (fieldConfig, fieldName) => {
     devAssert(
       !('resolve' in fieldConfig),
diff --git a/src/type/directives.ts b/src/type/directives.ts
index bb3e441a..13f4ad27 100644
--- a/src/type/directives.ts
+++ b/src/type/directives.ts
@@ -1,7 +1,5 @@
-import { devAssert } from '../jsutils/devAssert';
 import { inspect } from '../jsutils/inspect';
 import { instanceOf } from '../jsutils/instanceOf';
-import { isObjectLike } from '../jsutils/isObjectLike';
 import type { Maybe } from '../jsutils/Maybe';
 import { toObjMap } from '../jsutils/toObjMap';
 
@@ -70,17 +68,7 @@ export class GraphQLDirective {
     this.extensions = toObjMap(config.extensions);
     this.astNode = config.astNode;
 
-    devAssert(
-      Array.isArray(config.locations),
-      `@${config.name} locations must be an Array.`,
-    );
-
     const args = config.args ?? {};
-    devAssert(
-      isObjectLike(args) && !Array.isArray(args),
-      `@${config.name} args must be an object with argument names as keys.`,
-    );
-
     this.args = defineArguments(args);
   }
 
diff --git a/src/type/schema.ts b/src/type/schema.ts
index 7cc576a1..456c1772 100644
--- a/src/type/schema.ts
+++ b/src/type/schema.ts
@@ -1,7 +1,5 @@
-import { devAssert } from '../jsutils/devAssert';
 import { inspect } from '../jsutils/inspect';
 import { instanceOf } from '../jsutils/instanceOf';
-import { isObjectLike } from '../jsutils/isObjectLike';
 import type { Maybe } from '../jsutils/Maybe';
 import type { ObjMap } from '../jsutils/ObjMap';
 import { toObjMap } from '../jsutils/toObjMap';
@@ -159,18 +157,6 @@ export class GraphQLSchema {
     // marked with assumeValid to avoid an additional type system validation.
     this.__validationErrors = config.assumeValid === true ? [] : undefined;
 
-    // Check for common mistakes during construction to produce early errors.
-    devAssert(isObjectLike(config), 'Must provide configuration object.');
-    devAssert(
-      !config.types || Array.isArray(config.types),
-      `"types" must be Array if provided but got: ${inspect(config.types)}.`,
-    );
-    devAssert(
-      !config.directives || Array.isArray(config.directives),
-      '"directives" must be Array if provided but got: ' +
-        `${inspect(config.directives)}.`,
-    );
-
     this.description = config.description;
     this.extensions = toObjMap(config.extensions);
     this.astNode = config.astNode;
@@ -226,10 +212,6 @@ export class GraphQLSchema {
       }
 
       const typeName = namedType.name;
-      devAssert(
-        typeName != null,
-        'One of the provided types for building the Schema is missing a name.',
-      );
       if (this._typeMap[typeName] !== undefined) {
         throw new Error(
           `Schema must contain uniquely named types but contains multiple types named "${typeName}".`,
diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts
index eeff08e6..ce8267b3 100644
--- a/src/utilities/buildASTSchema.ts
+++ b/src/utilities/buildASTSchema.ts
@@ -1,7 +1,4 @@
-import { devAssert } from '../jsutils/devAssert';
-
 import type { DocumentNode } from '../language/ast';
-import { Kind } from '../language/kinds';
 import type { ParseOptions } from '../language/parser';
 import { parse } from '../language/parser';
 import type { Source } from '../language/source';
@@ -37,11 +34,6 @@ export function buildASTSchema(
   documentAST: DocumentNode,
   options?: BuildSchemaOptions,
 ): GraphQLSchema {
-  devAssert(
-    documentAST != null && documentAST.kind === Kind.DOCUMENT,
-    'Must provide valid Document AST.',
-  );
-
   if (options?.assumeValid !== true && options?.assumeValidSDL !== true) {
     assertValidSDL(documentAST);
   }
diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts
index 18e6110b..8418c6ce 100644
--- a/src/utilities/buildClientSchema.ts
+++ b/src/utilities/buildClientSchema.ts
@@ -65,6 +65,8 @@ export function buildClientSchema(
   introspection: IntrospectionQuery,
   options?: GraphQLSchemaValidationOptions,
 ): GraphQLSchema {
+  // Even even though `introspection` argument is typed in most cases it's received
+  // as untyped value from server, so we will do an additional check here.
   devAssert(
     isObjectLike(introspection) && isObjectLike(introspection.__schema),
     `Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: ${inspect(
diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts
index c451e1e6..e69b24be 100644
--- a/src/utilities/extendSchema.ts
+++ b/src/utilities/extendSchema.ts
@@ -1,4 +1,3 @@
-import { devAssert } from '../jsutils/devAssert';
 import { inspect } from '../jsutils/inspect';
 import { invariant } from '../jsutils/invariant';
 import { keyMap } from '../jsutils/keyMap';
@@ -111,11 +110,6 @@ export function extendSchema(
 ): GraphQLSchema {
   assertSchema(schema);
 
-  devAssert(
-    documentAST != null && documentAST.kind === Kind.DOCUMENT,
-    'Must provide valid Document AST.',
-  );
-
   if (options?.assumeValid !== true && options?.assumeValidSDL !== true) {
     assertValidSDLExtension(documentAST, schema);
   }
diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
index bdf6eb87..104a4e52 100644
--- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
+++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts
@@ -33,6 +33,10 @@ import { typeFromAST } from '../../utilities/typeFromAST';
 
 import type { ValidationContext } from '../ValidationContext';
 
+/* eslint-disable max-params */
+// This file contains a lot of such errors but we plan to refactor it anyway
+// so just disable it for entire file.
+
 function reasonMessage(reason: ConflictReasonMessage): string {
   if (Array.isArray(reason)) {
     return reason
diff --git a/src/validation/validate.ts b/src/validation/validate.ts
index 61ec1412..a9da292c 100644
--- a/src/validation/validate.ts
+++ b/src/validation/validate.ts
@@ -1,4 +1,3 @@
-import { devAssert } from '../jsutils/devAssert';
 import type { Maybe } from '../jsutils/Maybe';
 
 import { GraphQLError } from '../error/GraphQLError';
@@ -46,7 +45,6 @@ export function validate(
 ): ReadonlyArray<GraphQLError> {
   const maxErrors = options?.maxErrors ?? 100;
 
-  devAssert(documentAST != null, 'Must provide document.');
   // If the schema used for validation is invalid, throw an error.
   assertValidSchema(schema);
 


Test patch:
diff --git a/src/__testUtils__/kitchenSinkQuery.ts b/src/__testUtils__/kitchenSinkQuery.ts
index 9ed9a7e9..ff989d4b 100644
--- a/src/__testUtils__/kitchenSinkQuery.ts
+++ b/src/__testUtils__/kitchenSinkQuery.ts
@@ -10,6 +10,21 @@ query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
           ...frag @onFragmentSpread
         }
       }
+      
+      field3!
+      field4?
+      requiredField5: field5!
+      requiredSelectionSet(first: 10)! @directive {
+        field
+      }
+
+      unsetListItemsRequiredList: listField[]!
+      requiredListItemsUnsetList: listField[!]
+      requiredListItemsRequiredList: listField[!]!
+      unsetListItemsOptionalList: listField[]?
+      optionalListItemsUnsetList: listField[?]
+      optionalListItemsOptionalList: listField[?]?
+      multidimensionalList: listField[[[!]!]!]!
     }
     ... @skip(unless: $foo) {
       id
diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts
index bb9bf602..60b203dc 100644
--- a/src/execution/__tests__/executor-test.ts
+++ b/src/execution/__tests__/executor-test.ts
@@ -22,54 +22,6 @@ import { GraphQLSchema } from '../../type/schema';
 import { execute, executeSync } from '../execute';
 
 describe('Execute: Handles basic execution tasks', () => {
-  it('throws if no document is provided', () => {
-    const schema = new GraphQLSchema({
-      query: new GraphQLObjectType({
-        name: 'Type',
-        fields: {
-          a: { type: GraphQLString },
-        },
-      }),
-    });
-
-    // @ts-expect-error
-    expect(() => executeSync({ schema })).to.throw('Must provide document.');
-  });
-
-  it('throws if no schema is provided', () => {
-    const document = parse('{ field }');
-
-    // @ts-expect-error
-    expect(() => executeSync({ document })).to.throw(
-      'Expected undefined to be a GraphQL schema.',
-    );
-  });
-
-  it('throws on invalid variables', () => {
-    const schema = new GraphQLSchema({
-      query: new GraphQLObjectType({
-        name: 'Type',
-        fields: {
-          fieldA: {
-            type: GraphQLString,
-            args: { argA: { type: GraphQLInt } },
-          },
-        },
-      }),
-    });
-    const document = parse(`
-      query ($a: Int) {
-        fieldA(argA: $a)
-      }
-    `);
-    const variableValues = '{ "a": 1 }';
-
-    // @ts-expect-error
-    expect(() => executeSync({ schema, document, variableValues })).to.throw(
-      'Variables must be provided as an Object where each property is a variable value. Perhaps look to see if an unparsed JSON string was provided.',
-    );
-  });
-
   it('executes arbitrary code', async () => {
     const data = {
       a: () => 'Apple',
diff --git a/src/execution/__tests__/lists-test.ts b/src/execution/__tests__/lists-test.ts
index ac6460d5..3fdd77ab 100644
--- a/src/execution/__tests__/lists-test.ts
+++ b/src/execution/__tests__/lists-test.ts
@@ -3,10 +3,18 @@ import { describe, it } from 'mocha';
 
 import { expectJSON } from '../../__testUtils__/expectJSON';
 
+import type { PromiseOrValue } from '../../jsutils/PromiseOrValue';
+
 import { parse } from '../../language/parser';
 
+import type { GraphQLFieldResolver } from '../../type/definition';
+import { GraphQLList, GraphQLObjectType } from '../../type/definition';
+import { GraphQLString } from '../../type/scalars';
+import { GraphQLSchema } from '../../type/schema';
+
 import { buildSchema } from '../../utilities/buildASTSchema';
 
+import type { ExecutionResult } from '../execute';
 import { execute, executeSync } from '../execute';
 
 describe('Execute: Accepts any iterable as list value', () => {
@@ -66,6 +74,175 @@ describe('Execute: Accepts any iterable as list value', () => {
   });
 });
 
+describe('Execute: Accepts async iterables as list value', () => {
+  function complete(rootValue: unknown, as: string = '[String]') {
+    return execute({
+      schema: buildSchema(`type Query { listField: ${as} }`),
+      document: parse('{ listField }'),
+      rootValue,
+    });
+  }
+
+  function completeObjectList(
+    resolve: GraphQLFieldResolver<{ index: number }, unknown>,
+  ): PromiseOrValue<ExecutionResult> {
+    const schema = new GraphQLSchema({
+      query: new GraphQLObjectType({
+        name: 'Query',
+        fields: {
+          listField: {
+            resolve: async function* listField() {
+              yield await Promise.resolve({ index: 0 });
+              yield await Promise.resolve({ index: 1 });
+              yield await Promise.resolve({ index: 2 });
+            },
+            type: new GraphQLList(
+              new GraphQLObjectType({
+                name: 'ObjectWrapper',
+                fields: {
+                  index: {
+                    type: GraphQLString,
+                    resolve,
+                  },
+                },
+              }),
+            ),
+          },
+        },
+      }),
+    });
+    return execute({
+      schema,
+      document: parse('{ listField { index } }'),
+    });
+  }
+
+  it('Accepts an AsyncGenerator function as a List value', async () => {
+    async function* listField() {
+      yield await Promise.resolve('two');
+      yield await Promise.resolve(4);
+      yield await Promise.resolve(false);
+    }
+
+    expectJSON(await complete({ listField })).toDeepEqual({
+      data: { listField: ['two', '4', 'false'] },
+    });
+  });
+
+  it('Handles an AsyncGenerator function that throws', async () => {
+    async function* listField() {
+      yield await Promise.resolve('two');
+      yield await Promise.resolve(4);
+      throw new Error('bad');
+    }
+
+    expectJSON(await complete({ listField })).toDeepEqual({
+      data: { listField: ['two', '4', null] },
+      errors: [
+        {
+          message: 'bad',
+          locations: [{ line: 1, column: 3 }],
+          path: ['listField', 2],
+        },
+      ],
+    });
+  });
+
+  it('Handles an AsyncGenerator function where an intermediate value triggers an error', async () => {
+    async function* listField() {
+      yield await Promise.resolve('two');
+      yield await Promise.resolve({});
+      yield await Promise.resolve(4);
+    }
+
+    expectJSON(await complete({ listField })).toDeepEqual({
+      data: { listField: ['two', null, '4'] },
+      errors: [
+        {
+          message: 'String cannot represent value: {}',
+          locations: [{ line: 1, column: 3 }],
+          path: ['listField', 1],
+        },
+      ],
+    });
+  });
+
+  it('Handles errors from `completeValue` in AsyncIterables', async () => {
+    async function* listField() {
+      yield await Promise.resolve('two');
+      yield await Promise.resolve({});
+    }
+
+    expectJSON(await complete({ listField })).toDeepEqual({
+      data: { listField: ['two', null] },
+      errors: [
+        {
+          message: 'String cannot represent value: {}',
+          locations: [{ line: 1, column: 3 }],
+          path: ['listField', 1],
+        },
+      ],
+    });
+  });
+
+  it('Handles promises from `completeValue` in AsyncIterables', async () => {
+    expectJSON(
+      await completeObjectList(({ index }) => Promise.resolve(index)),
+    ).toDeepEqual({
+      data: { listField: [{ index: '0' }, { index: '1' }, { index: '2' }] },
+    });
+  });
+
+  it('Handles rejected promises from `completeValue` in AsyncIterables', async () => {
+    expectJSON(
+      await completeObjectList(({ index }) => {
+        if (index === 2) {
+          return Promise.reject(new Error('bad'));
+        }
+        return Promise.resolve(index);
+      }),
+    ).toDeepEqual({
+      data: { listField: [{ index: '0' }, { index: '1' }, { index: null }] },
+      errors: [
+        {
+          message: 'bad',
+          locations: [{ line: 1, column: 15 }],
+          path: ['listField', 2, 'index'],
+        },
+      ],
+    });
+  });
+  it('Handles nulls yielded by async generator', async () => {
+    async function* listField() {
+      yield await Promise.resolve(1);
+      yield await Promise.resolve(null);
+      yield await Promise.resolve(2);
+    }
+    const errors = [
+      {
+        message: 'Cannot return null for non-nullable field Query.listField.',
+        locations: [{ line: 1, column: 3 }],
+        path: ['listField', 1],
+      },
+    ];
+
+    expect(await complete({ listField }, '[Int]')).to.deep.equal({
+      data: { listField: [1, null, 2] },
+    });
+    expect(await complete({ listField }, '[Int]!')).to.deep.equal({
+      data: { listField: [1, null, 2] },
+    });
+    expectJSON(await complete({ listField }, '[Int!]')).toDeepEqual({
+      data: { listField: null },
+      errors,
+    });
+    expectJSON(await complete({ listField }, '[Int!]!')).toDeepEqual({
+      data: null,
+      errors,
+    });
+  });
+});
+
 describe('Execute: Handles list nullability', () => {
   async function complete(args: { listField: unknown; as: string }) {
     const { listField, as } = args;
diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts
index b095f2b5..5f256ca8 100644
--- a/src/execution/__tests__/subscribe-test.ts
+++ b/src/execution/__tests__/subscribe-test.ts
@@ -5,6 +5,8 @@ import { expectJSON } from '../../__testUtils__/expectJSON';
 import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick';
 
 import { isAsyncIterable } from '../../jsutils/isAsyncIterable';
+import { isPromise } from '../../jsutils/isPromise';
+import type { PromiseOrValue } from '../../jsutils/PromiseOrValue';
 
 import { parse } from '../../language/parser';
 
@@ -12,7 +14,8 @@ import { GraphQLList, GraphQLObjectType } from '../../type/definition';
 import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars';
 import { GraphQLSchema } from '../../type/schema';
 
-import { createSourceEventStream, subscribe } from '../subscribe';
+import type { ExecutionArgs, ExecutionResult } from '../execute';
+import { createSourceEventStream, subscribe } from '../execute';
 
 import { SimplePubSub } from './simplePubSub';
 
@@ -122,28 +125,49 @@ function createSubscription(pubsub: SimplePubSub<Email>) {
   return subscribe({ schema: emailSchema, document, rootValue: data });
 }
 
-async function expectPromise(promise: Promise<unknown>) {
-  let caughtError: Error;
-
-  try {
-    /* c8 ignore next 2 */
-    await promise;
-    expect.fail('promise should have thrown but did not');
-  } catch (error) {
-    caughtError = error;
-  }
+// TODO: consider adding this method to testUtils (with tests)
+function expectPromise(maybePromise: unknown) {
+  assert(isPromise(maybePromise));
 
   return {
-    toReject() {
-      expect(caughtError).to.be.an.instanceOf(Error);
+    toResolve() {
+      return maybePromise;
     },
-    toRejectWith(message: string) {
+    async toRejectWith(message: string) {
+      let caughtError: Error;
+
+      try {
+        /* c8 ignore next 2 */
+        await maybePromise;
+        expect.fail('promise should have thrown but did not');
+      } catch (error) {
+        caughtError = error;
+      }
+
       expect(caughtError).to.be.an.instanceOf(Error);
       expect(caughtError).to.have.property('message', message);
     },
   };
 }
 
+// TODO: consider adding this method to testUtils (with tests)
+function expectEqualPromisesOrValues<T>(
+  value1: PromiseOrValue<T>,
+  value2: PromiseOrValue<T>,
+): PromiseOrValue<T> {
+  if (isPromise(value1)) {
+    assert(isPromise(value2));
+    return Promise.all([value1, value2]).then((resolved) => {
+      expectJSON(resolved[1]).toDeepEqual(resolved[0]);
+      return resolved[0];
+    });
+  }
+
+  assert(!isPromise(value2));
+  expectJSON(value2).toDeepEqual(value1);
+  return value1;
+}
+
 const DummyQueryType = new GraphQLObjectType({
   name: 'Query',
   fields: {
@@ -151,6 +175,32 @@ const DummyQueryType = new GraphQLObjectType({
   },
 });
 
+function subscribeWithBadFn(
+  subscribeFn: () => unknown,
+): PromiseOrValue<ExecutionResult | AsyncIterable<unknown>> {
+  const schema = new GraphQLSchema({
+    query: DummyQueryType,
+    subscription: new GraphQLObjectType({
+      name: 'Subscription',
+      fields: {
+        foo: { type: GraphQLString, subscribe: subscribeFn },
+      },
+    }),
+  });
+  const document = parse('subscription { foo }');
+
+  return subscribeWithBadArgs({ schema, document });
+}
+
+function subscribeWithBadArgs(
+  args: ExecutionArgs,
+): PromiseOrValue<ExecutionResult | AsyncIterable<unknown>> {
+  return expectEqualPromisesOrValues(
+    subscribe(args),
+    createSourceEventStream(args),
+  );
+}
+
 /* eslint-disable @typescript-eslint/require-await */
 // Check all error cases when initializing the subscription.
 describe('Subscription Initialization Phase', () => {
@@ -170,7 +220,7 @@ describe('Subscription Initialization Phase', () => {
       yield { foo: 'FooValue' };
     }
 
-    const subscription = await subscribe({
+    const subscription = subscribe({
       schema,
       document: parse('subscription { foo }'),
       rootValue: { foo: fooGenerator },
@@ -206,7 +256,7 @@ describe('Subscription Initialization Phase', () => {
       }),
     });
 
-    const subscription = await subscribe({
+    const subscription = subscribe({
       schema,
       document: parse('subscription { foo }'),
     });
@@ -244,10 +294,13 @@ describe('Subscription Initialization Phase', () => {
       }),
     });
 
-    const subscription = await subscribe({
+    const promise = subscribe({
       schema,
       document: parse('subscription { foo }'),
     });
+    assert(isPromise(promise));
+
+    const subscription = await promise;
     assert(isAsyncIterable(subscription));
 
     expect(await subscription.next()).to.deep.equal({
@@ -276,7 +329,7 @@ describe('Subscription Initialization Phase', () => {
       yield { foo: 'FooValue' };
     }
 
-    const subscription = await subscribe({
+    const subscription = subscribe({
       schema,
       document: parse('subscription { foo }'),
       rootValue: { customFoo: fooGenerator },
@@ -326,7 +379,7 @@ describe('Subscription Initialization Phase', () => {
       }),
     });
 
-    const subscription = await subscribe({
+    const subscription = subscribe({
       schema,
       document: parse('subscription { foo bar }'),
     });
@@ -343,44 +396,11 @@ describe('Subscription Initialization Phase', () => {
     });
   });
 
-  it('throws an error if some of required arguments are missing', async () => {
-    const document = parse('subscription { foo }');
-    const schema = new GraphQLSchema({
-      query: DummyQueryType,
-      subscription: new GraphQLObjectType({
-        name: 'Subscription',
-        fields: {
-          foo: { type: GraphQLString },
-        },
-      }),
-    });
-
-    // @ts-expect-error (schema must not be null)
-    (await expectPromise(subscribe({ schema: null, document }))).toRejectWith(
-      'Expected null to be a GraphQL schema.',
-    );
-
-    // @ts-expect-error
-    (await expectPromise(subscribe({ document }))).toRejectWith(
-      'Expected undefined to be a GraphQL schema.',
-    );
-
-    // @ts-expect-error (document must not be null)
-    (await expectPromise(subscribe({ schema, document: null }))).toRejectWith(
-      'Must provide document.',
-    );
-
-    // @ts-expect-error
-    (await expectPromise(subscribe({ schema }))).toRejectWith(
-      'Must provide document.',
-    );
-  });
-
   it('resolves to an error if schema does not support subscriptions', async () => {
     const schema = new GraphQLSchema({ query: DummyQueryType });
     const document = parse('subscription { unknownField }');
 
-    const result = await subscribe({ schema, document });
+    const result = subscribeWithBadArgs({ schema, document });
     expectJSON(result).toDeepEqual({
       errors: [
         {
@@ -404,7 +424,7 @@ describe('Subscription Initialization Phase', () => {
     });
     const document = parse('subscription { unknownField }');
 
-    const result = await subscribe({ schema, document });
+    const result = subscribeWithBadArgs({ schema, document });
     expectJSON(result).toDeepEqual({
       errors: [
         {
@@ -427,50 +447,31 @@ describe('Subscription Initialization Phase', () => {
     });
 
     // @ts-expect-error
-    (await expectPromise(subscribe({ schema, document: {} }))).toReject();
+    expect(() => subscribeWithBadArgs({ schema, document: {} })).to.throw();
   });
 
   it('throws an error if subscribe does not return an iterator', async () => {
-    const schema = new GraphQLSchema({
-      query: DummyQueryType,
-      subscription: new GraphQLObjectType({
-        name: 'Subscription',
-        fields: {
-          foo: {
-            type: GraphQLString,
-            subscribe: () => 'test',
-          },
+    const expectedResult = {
+      errors: [
+        {
+          message:
+            'Subscription field must return Async Iterable. Received: "test".',
+          locations: [{ line: 1, column: 16 }],
+          path: ['foo'],
         },
-      }),
-    });
+      ],
+    };
 
-    const document = parse('subscription { foo }');
+    expectJSON(subscribeWithBadFn(() => 'test')).toDeepEqual(expectedResult);
 
-    (await expectPromise(subscribe({ schema, document }))).toRejectWith(
-      'Subscription field must return Async Iterable. Received: "test".',
-    );
+    expectJSON(
+      await expectPromise(
+        subscribeWithBadFn(() => Promise.resolve('test')),
+      ).toResolve(),
+    ).toDeepEqual(expectedResult);
   });
 
   it('resolves to an error for subscription resolver errors', async () => {
-    async function subscribeWithFn(subscribeFn: () => unknown) {
-      const schema = new GraphQLSchema({
-        query: DummyQueryType,
-        subscription: new GraphQLObjectType({
-          name: 'Subscription',
-          fields: {
-            foo: { type: GraphQLString, subscribe: subscribeFn },
-          },
-        }),
-      });
-      const document = parse('subscription { foo }');
-      const result = await subscribe({ schema, document });
-
-      expectJSON(await createSourceEventStream(schema, document)).toDeepEqual(
-        result,
-      );
-      return result;
-    }
-
     const expectedResult = {
       errors: [
         {
@@ -483,24 +484,28 @@ describe('Subscription Initialization Phase', () => {
 
     expectJSON(
       // Returning an error
-      await subscribeWithFn(() => new Error('test error')),
+      subscribeWithBadFn(() => new Error('test error')),
     ).toDeepEqual(expectedResult);
 
     expectJSON(
       // Throwing an error
-      await subscribeWithFn(() => {
+      subscribeWithBadFn(() => {
         throw new Error('test error');
       }),
     ).toDeepEqual(expectedResult);
 
     expectJSON(
       // Resolving to an error
-      await subscribeWithFn(() => Promise.resolve(new Error('test error'))),
+      await expectPromise(
+        subscribeWithBadFn(() => Promise.resolve(new Error('test error'))),
+      ).toResolve(),
     ).toDeepEqual(expectedResult);
 
     expectJSON(
       // Rejecting with an error
-      await subscribeWithFn(() => Promise.reject(new Error('test error'))),
+      await expectPromise(
+        subscribeWithBadFn(() => Promise.reject(new Error('test error'))),
+      ).toResolve(),
     ).toDeepEqual(expectedResult);
   });
 
@@ -527,7 +532,7 @@ describe('Subscription Initialization Phase', () => {
 
     // If we receive variables that cannot be coerced correctly, subscribe() will
     // resolve to an ExecutionResult that contains an informative error description.
-    const result = await subscribe({ schema, document, variableValues });
+    const result = subscribeWithBadArgs({ schema, document, variableValues });
     expectJSON(result).toDeepEqual({
       errors: [
         {
@@ -545,10 +550,10 @@ describe('Subscription Publish Phase', () => {
   it('produces a payload for multiple subscribe in same subscription', async () => {
     const pubsub = new SimplePubSub<Email>();
 
-    const subscription = await createSubscription(pubsub);
+    const subscription = createSubscription(pubsub);
     assert(isAsyncIterable(subscription));
 
-    const secondSubscription = await createSubscription(pubsub);
+    const secondSubscription = createSubscription(pubsub);
     assert(isAsyncIterable(secondSubscription));
 
     const payload1 = subscription.next();
@@ -587,7 +592,7 @@ describe('Subscription Publish Phase', () => {
 
   it('produces a payload per subscription event', async () => {
     const pubsub = new SimplePubSub<Email>();
-    const subscription = await createSubscription(pubsub);
+    const subscription = createSubscription(pubsub);
     assert(isAsyncIterable(subscription));
 
     // Wait for the next subscription payload.
@@ -676,7 +681,7 @@ describe('Subscription Publish Phase', () => {
 
   it('produces a payload when there are multiple events', async () => {
     const pubsub = new SimplePubSub<Email>();
-    const subscription = await createSubscription(pubsub);
+    const subscription = createSubscription(pubsub);
     assert(isAsyncIterable(subscription));
 
     let payload = subscription.next();
@@ -742,7 +747,7 @@ describe('Subscription Publish Phase', () => {
 
   it('should not trigger when subscription is already done', async () => {
     const pubsub = new SimplePubSub<Email>();
-    const subscription = await createSubscription(pubsub);
+    const subscription = createSubscription(pubsub);
     assert(isAsyncIterable(subscription));
 
     let payload = subscription.next();
@@ -796,7 +801,7 @@ describe('Subscription Publish Phase', () => {
 
   it('should not trigger when subscription is thrown', async () => {
     const pubsub = new SimplePubSub<Email>();
-    const subscription = await createSubscription(pubsub);
+    const subscription = createSubscription(pubsub);
     assert(isAsyncIterable(subscription));
 
     let payload = subscription.next();
@@ -849,7 +854,7 @@ describe('Subscription Publish Phase', () => {
 
   it('event order is correct for multiple publishes', async () => {
     const pubsub = new SimplePubSub<Email>();
-    const subscription = await createSubscription(pubsub);
+    const subscription = createSubscription(pubsub);
     assert(isAsyncIterable(subscription));
 
     let payload = subscription.next();
@@ -940,7 +945,7 @@ describe('Subscription Publish Phase', () => {
     });
 
     const document = parse('subscription { newMessage }');
-    const subscription = await subscribe({ schema, document });
+    const subscription = subscribe({ schema, document });
     assert(isAsyncIterable(subscription));
 
     expect(await subscription.next()).to.deep.equal({
@@ -1001,7 +1006,7 @@ describe('Subscription Publish Phase', () => {
     });
 
     const document = parse('subscription { newMessage }');
-    const subscription = await subscribe({ schema, document });
+    const subscription = subscribe({ schema, document });
     assert(isAsyncIterable(subscription));
 
     expect(await subscription.next()).to.deep.equal({
@@ -1011,7 +1016,7 @@ describe('Subscription Publish Phase', () => {
       },
     });
 
-    (await expectPromise(subscription.next())).toRejectWith('test error');
+    await expectPromise(subscription.next()).toRejectWith('test error');
 
     expect(await subscription.next()).to.deep.equal({
       done: true,
diff --git a/src/language/__tests__/lexer-test.ts b/src/language/__tests__/lexer-test.ts
index 46bf971d..0f19d860 100644
--- a/src/language/__tests__/lexer-test.ts
+++ b/src/language/__tests__/lexer-test.ts
@@ -936,6 +936,13 @@ describe('Lexer', () => {
       value: undefined,
     });
 
+    expect(lexOne('?')).to.contain({
+      kind: TokenKind.QUESTION_MARK,
+      start: 0,
+      end: 1,
+      value: undefined,
+    });
+
     expect(lexOne('$')).to.contain({
       kind: TokenKind.DOLLAR,
       start: 0,
@@ -1181,6 +1188,7 @@ describe('isPunctuatorTokenKind', () => {
 
   it('returns true for punctuator tokens', () => {
     expect(isPunctuatorToken('!')).to.equal(true);
+    expect(isPunctuatorToken('?')).to.equal(true);
     expect(isPunctuatorToken('$')).to.equal(true);
     expect(isPunctuatorToken('&')).to.equal(true);
     expect(isPunctuatorToken('(')).to.equal(true);
diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts
index 3571b757..651da1ca 100644
--- a/src/language/__tests__/parser-test.ts
+++ b/src/language/__tests__/parser-test.ts
@@ -12,6 +12,10 @@ import { parse, parseConstValue, parseType, parseValue } from '../parser';
 import { Source } from '../source';
 import { TokenKind } from '../tokenKind';
 
+function parseCCN(source: string) {
+  return parse(source, { experimentalClientControlledNullability: true });
+}
+
 function expectSyntaxError(text: string) {
   return expectToThrowJSON(() => parse(text));
 }
@@ -153,7 +157,7 @@ describe('Parser', () => {
   });
 
   it('parses kitchen sink', () => {
-    expect(() => parse(kitchenSinkQuery)).to.not.throw();
+    expect(() => parseCCN(kitchenSinkQuery)).to.not.throw();
   });
 
   it('allows non-keywords anywhere a Name is allowed', () => {
@@ -224,6 +228,206 @@ describe('Parser', () => {
     ).to.not.throw();
   });
 
+  it('parses required field', () => {
+    const result = parseCCN('{ requiredField! }');
+
+    expectJSON(result).toDeepNestedProperty(
+      'definitions[0].selectionSet.selections[0].nullabilityAssertion',
+      {
+        kind: Kind.NON_NULL_ASSERTION,
+        loc: { start: 15, end: 16 },
+        nullabilityAssertion: undefined,
+      },
+    );
+  });
+
+  it('parses optional field', () => {
+    expect(() => parseCCN('{ optionalField? }')).to.not.throw();
+  });
+
+  it('does not parse field with multiple designators', () => {
+    expect(() => parseCCN('{ optionalField?! }')).to.throw(
+      'Syntax Error: Expected Name, found "!".',
+    );
+
+    expect(() => parseCCN('{ optionalField!? }')).to.throw(
+      'Syntax Error: Expected Name, found "?".',
+    );
+  });
+
+  it('parses required with alias', () => {
+    expect(() => parseCCN('{ requiredField: field! }')).to.not.throw();
+  });
+
+  it('parses optional with alias', () => {
+    expect(() => parseCCN('{ requiredField: field? }')).to.not.throw();
+  });
+
+  it('does not parse aliased field with bang on left of colon', () => {
+    expect(() => parseCCN('{ requiredField!: field }')).to.throw();
+  });
+
+  it('does not parse aliased field with question mark on left of colon', () => {
+    expect(() => parseCCN('{ requiredField?: field }')).to.throw();
+  });
+
+  it('does not parse aliased field with bang on left and right of colon', () => {
+    expect(() => parseCCN('{ requiredField!: field! }')).to.throw();
+  });
+
+  it('does not parse aliased field with question mark on left and right of colon', () => {
+    expect(() => parseCCN('{ requiredField?: field? }')).to.throw();
+  });
+
+  it('does not parse designator on query', () => {
+    expect(() => parseCCN('query? { field }')).to.throw();
+  });
+
+  it('parses required within fragment', () => {
+    expect(() =>
+      parseCCN('fragment MyFragment on Query { field! }'),
+    ).to.not.throw();
+  });
+
+  it('parses optional within fragment', () => {
+    expect(() =>
+      parseCCN('fragment MyFragment on Query { field? }'),
+    ).to.not.throw();
+  });
+
+  it('parses field with required list elements', () => {
+    const result = parseCCN('{ field[!] }');
+
+    expectJSON(result).toDeepNestedProperty(
+      'definitions[0].selectionSet.selections[0].nullabilityAssertion',
+      {
+        kind: Kind.LIST_NULLABILITY_OPERATOR,
+        loc: { start: 7, end: 10 },
+        nullabilityAssertion: {
+          kind: Kind.NON_NULL_ASSERTION,
+          loc: { start: 8, end: 9 },
+          nullabilityAssertion: undefined,
+        },
+      },
+    );
+  });
+
+  it('parses field with optional list elements', () => {
+    const result = parseCCN('{ field[?] }');
+
+    expectJSON(result).toDeepNestedProperty(
+      'definitions[0].selectionSet.selections[0].nullabilityAssertion',
+      {
+        kind: Kind.LIST_NULLABILITY_OPERATOR,
+        loc: { start: 7, end: 10 },
+        nullabilityAssertion: {
+          kind: Kind.ERROR_BOUNDARY,
+          loc: { start: 8, end: 9 },
+          nullabilityAssertion: undefined,
+        },
+      },
+    );
+  });
+
+  it('parses field with required list', () => {
+    const result = parseCCN('{ field[]! }');
+
+    expectJSON(result).toDeepNestedProperty(
+      'definitions[0].selectionSet.selections[0].nullabilityAssertion',
+      {
+        kind: Kind.NON_NULL_ASSERTION,
+        loc: { start: 7, end: 10 },
+        nullabilityAssertion: {
+          kind: Kind.LIST_NULLABILITY_OPERATOR,
+          loc: { start: 7, end: 9 },
+          nullabilityAssertion: undefined,
+        },
+      },
+    );
+  });
+
+  it('parses field with optional list', () => {
+    const result = parseCCN('{ field[]? }');
+
+    expectJSON(result).toDeepNestedProperty(
+      'definitions[0].selectionSet.selections[0].nullabilityAssertion',
+      {
+        kind: Kind.ERROR_BOUNDARY,
+        loc: { start: 7, end: 10 },
+        nullabilityAssertion: {
+          kind: Kind.LIST_NULLABILITY_OPERATOR,
+          loc: { start: 7, end: 9 },
+          nullabilityAssertion: undefined,
+        },
+      },
+    );
+  });
+
+  it('parses multidimensional field with mixed list elements', () => {
+    const result = parseCCN('{ field[[[?]!]]! }');
+
+    expectJSON(result).toDeepNestedProperty(
+      'definitions[0].selectionSet.selections[0].nullabilityAssertion',
+      {
+        kind: Kind.NON_NULL_ASSERTION,
+        loc: { start: 7, end: 16 },
+        nullabilityAssertion: {
+          kind: Kind.LIST_NULLABILITY_OPERATOR,
+          loc: { start: 7, end: 15 },
+          nullabilityAssertion: {
+            kind: Kind.LIST_NULLABILITY_OPERATOR,
+            loc: { start: 8, end: 14 },
+            nullabilityAssertion: {
+              kind: Kind.NON_NULL_ASSERTION,
+              loc: { start: 9, end: 13 },
+              nullabilityAssertion: {
+                kind: Kind.LIST_NULLABILITY_OPERATOR,
+                loc: { start: 9, end: 12 },
+                nullabilityAssertion: {
+                  kind: Kind.ERROR_BOUNDARY,
+                  loc: { start: 10, end: 11 },
+                  nullabilityAssertion: undefined,
+                },
+              },
+            },
+          },
+        },
+      },
+    );
+  });
+
+  it('does not parse field with unbalanced brackets', () => {
+    expect(() => parseCCN('{ field[[] }')).to.throw(
+      'Syntax Error: Expected "]", found "}".',
+    );
+
+    expect(() => parseCCN('{ field[]] }')).to.throw(
+      'Syntax Error: Expected Name, found "]".',
+    );
+
+    expect(() => parse('{ field] }')).to.throw(
+      'Syntax Error: Expected Name, found "]".',
+    );
+
+    expect(() => parseCCN('{ field[ }')).to.throw(
+      'Syntax Error: Expected "]", found "}".',
+    );
+  });
+
+  it('does not parse field with assorted invalid nullability designators', () => {
+    expect(() => parseCCN('{ field[][] }')).to.throw(
+      'Syntax Error: Expected Name, found "[".',
+    );
+
+    expect(() => parseCCN('{ field[!!] }')).to.throw(
+      'Syntax Error: Expected "]", found "!".',
+    );
+
+    expect(() => parseCCN('{ field[]?! }')).to.throw(
+      'Syntax Error: Expected Name, found "!".',
+    );
+  });
+
   it('creates ast', () => {
     const result = parse(dedent`
       {
@@ -274,6 +478,7 @@ describe('Parser', () => {
                     loc: { start: 9, end: 14 },
                   },
                 ],
+                nullabilityAssertion: undefined,
                 directives: [],
                 selectionSet: {
                   kind: Kind.SELECTION_SET,
@@ -289,6 +494,7 @@ describe('Parser', () => {
                         value: 'id',
                       },
                       arguments: [],
+                      nullabilityAssertion: undefined,
                       directives: [],
                       selectionSet: undefined,
                     },
@@ -302,6 +508,7 @@ describe('Parser', () => {
                         value: 'name',
                       },
                       arguments: [],
+                      nullabilityAssertion: undefined,
                       directives: [],
                       selectionSet: undefined,
                     },
@@ -349,6 +556,7 @@ describe('Parser', () => {
                   value: 'node',
                 },
                 arguments: [],
+                nullabilityAssertion: undefined,
                 directives: [],
                 selectionSet: {
                   kind: Kind.SELECTION_SET,
@@ -364,6 +572,7 @@ describe('Parser', () => {
                         value: 'id',
                       },
                       arguments: [],
+                      nullabilityAssertion: undefined,
                       directives: [],
                       selectionSet: undefined,
                     },
diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts
index 13477f8d..341a5715 100644
--- a/src/language/__tests__/predicates-test.ts
+++ b/src/language/__tests__/predicates-test.ts
@@ -8,6 +8,7 @@ import {
   isConstValueNode,
   isDefinitionNode,
   isExecutableDefinitionNode,
+  isNullabilityAssertionNode,
   isSelectionNode,
   isTypeDefinitionNode,
   isTypeExtensionNode,
@@ -62,6 +63,14 @@ describe('AST node predicates', () => {
     ]);
   });
 
+  it('isNullabilityAssertionNode', () => {
+    expect(filterNodes(isNullabilityAssertionNode)).to.deep.equal([
+      'ListNullabilityOperator',
+      'NonNullAssertion',
+      'ErrorBoundary',
+    ]);
+  });
+
   it('isValueNode', () => {
     expect(filterNodes(isValueNode)).to.deep.equal([
       'Variable',
diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts
index 73e760d6..10501647 100644
--- a/src/language/__tests__/printer-test.ts
+++ b/src/language/__tests__/printer-test.ts
@@ -139,11 +139,17 @@ describe('Printer: Query document', () => {
   });
 
   it('prints kitchen sink without altering ast', () => {
-    const ast = parse(kitchenSinkQuery, { noLocation: true });
+    const ast = parse(kitchenSinkQuery, {
+      noLocation: true,
+      experimentalClientControlledNullability: true,
+    });
 
     const astBeforePrintCall = JSON.stringify(ast);
     const printed = print(ast);
-    const printedAST = parse(printed, { noLocation: true });
+    const printedAST = parse(printed, {
+      noLocation: true,
+      experimentalClientControlledNullability: true,
+    });
 
     expect(printedAST).to.deep.equal(ast);
     expect(JSON.stringify(ast)).to.equal(astBeforePrintCall);
@@ -161,6 +167,19 @@ describe('Printer: Query document', () => {
                 ...frag @onFragmentSpread
               }
             }
+            field3!
+            field4?
+            requiredField5: field5!
+            requiredSelectionSet(first: 10)! @directive {
+              field
+            }
+            unsetListItemsRequiredList: listField[]!
+            requiredListItemsUnsetList: listField[!]
+            requiredListItemsRequiredList: listField[!]!
+            unsetListItemsOptionalList: listField[]?
+            optionalListItemsUnsetList: listField[?]
+            optionalListItemsOptionalList: listField[?]?
+            multidimensionalList: listField[[[!]!]!]!
           }
           ... @skip(unless: $foo) {
             id
diff --git a/src/language/__tests__/source-test.ts b/src/language/__tests__/source-test.ts
index 6bf8a93e..1e7d2460 100644
--- a/src/language/__tests__/source-test.ts
+++ b/src/language/__tests__/source-test.ts
@@ -4,20 +4,6 @@ import { describe, it } from 'mocha';
 import { Source } from '../source';
 
 describe('Source', () => {
-  it('asserts that a body was provided', () => {
-    // @ts-expect-error
-    expect(() => new Source()).to.throw(
-      'Body must be a string. Received: undefined.',
-    );
-  });
-
-  it('asserts that a valid body was provided', () => {
-    // @ts-expect-error
-    expect(() => new Source({})).to.throw(
-      'Body must be a string. Received: {}.',
-    );
-  });
-
   it('can be Object.toStringified', () => {
     const source = new Source('');
 
diff --git a/src/language/__tests__/visitor-test.ts b/src/language/__tests__/visitor-test.ts
index 9149b103..a15f3df4 100644
--- a/src/language/__tests__/visitor-test.ts
+++ b/src/language/__tests__/visitor-test.ts
@@ -505,8 +505,10 @@ describe('Visitor', () => {
     ]);
   });
 
-  it('visits kitchen sink', () => {
-    const ast = parse(kitchenSinkQuery);
+  it('n', () => {
+    const ast = parse(kitchenSinkQuery, {
+      experimentalClientControlledNullability: true,
+    });
     const visited: Array<any> = [];
     const argsStack: Array<any> = [];
 
@@ -654,6 +656,272 @@ describe('Visitor', () => {
       ['leave', 'Field', 1, undefined],
       ['leave', 'SelectionSet', 'selectionSet', 'Field'],
       ['leave', 'Field', 0, undefined],
+      ['enter', 'Field', 1, undefined],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      ['leave', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 1, undefined],
+      ['enter', 'Field', 2, undefined],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'ErrorBoundary', 'nullabilityAssertion', 'Field'],
+      ['leave', 'ErrorBoundary', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 2, undefined],
+      ['enter', 'Field', 3, undefined],
+      ['enter', 'Name', 'alias', 'Field'],
+      ['leave', 'Name', 'alias', 'Field'],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      ['leave', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 3, undefined],
+      ['enter', 'Field', 4, undefined],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'Argument', 0, undefined],
+      ['enter', 'Name', 'name', 'Argument'],
+      ['leave', 'Name', 'name', 'Argument'],
+      ['enter', 'IntValue', 'value', 'Argument'],
+      ['leave', 'IntValue', 'value', 'Argument'],
+      ['leave', 'Argument', 0, undefined],
+      ['enter', 'Directive', 0, undefined],
+      ['enter', 'Name', 'name', 'Directive'],
+      ['leave', 'Name', 'name', 'Directive'],
+      ['leave', 'Directive', 0, undefined],
+      ['enter', 'SelectionSet', 'selectionSet', 'Field'],
+      ['enter', 'Field', 0, undefined],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['leave', 'Field', 0, undefined],
+      ['leave', 'SelectionSet', 'selectionSet', 'Field'],
+      ['enter', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      ['leave', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 4, undefined],
+      ['enter', 'Field', 5, undefined],
+      ['enter', 'Name', 'alias', 'Field'],
+      ['leave', 'Name', 'alias', 'Field'],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      [
+        'enter',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      [
+        'leave',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      ['leave', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 5, undefined],
+      ['enter', 'Field', 6, undefined],
+      ['enter', 'Name', 'alias', 'Field'],
+      ['leave', 'Name', 'alias', 'Field'],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'ListNullabilityOperator', 'nullabilityAssertion', 'Field'],
+      [
+        'enter',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      ['leave', 'ListNullabilityOperator', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 6, undefined],
+      ['enter', 'Field', 7, undefined],
+      ['enter', 'Name', 'alias', 'Field'],
+      ['leave', 'Name', 'alias', 'Field'],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      [
+        'enter',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      [
+        'enter',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      ['leave', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 7, undefined],
+      ['enter', 'Field', 8, undefined],
+      ['enter', 'Name', 'alias', 'Field'],
+      ['leave', 'Name', 'alias', 'Field'],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'ErrorBoundary', 'nullabilityAssertion', 'Field'],
+      [
+        'enter',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'ErrorBoundary',
+      ],
+      [
+        'leave',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'ErrorBoundary',
+      ],
+      ['leave', 'ErrorBoundary', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 8, undefined],
+      ['enter', 'Field', 9, undefined],
+      ['enter', 'Name', 'alias', 'Field'],
+      ['leave', 'Name', 'alias', 'Field'],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'ListNullabilityOperator', 'nullabilityAssertion', 'Field'],
+      [
+        'enter',
+        'ErrorBoundary',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'ErrorBoundary',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      ['leave', 'ListNullabilityOperator', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 9, undefined],
+      ['enter', 'Field', 10, undefined],
+      ['enter', 'Name', 'alias', 'Field'],
+      ['leave', 'Name', 'alias', 'Field'],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'ErrorBoundary', 'nullabilityAssertion', 'Field'],
+      [
+        'enter',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'ErrorBoundary',
+      ],
+      [
+        'enter',
+        'ErrorBoundary',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'ErrorBoundary',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'ErrorBoundary',
+      ],
+      ['leave', 'ErrorBoundary', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 10, undefined],
+      ['enter', 'Field', 11, undefined],
+      ['enter', 'Name', 'alias', 'Field'],
+      ['leave', 'Name', 'alias', 'Field'],
+      ['enter', 'Name', 'name', 'Field'],
+      ['leave', 'Name', 'name', 'Field'],
+      ['enter', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      [
+        'enter',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      [
+        'enter',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'enter',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      [
+        'enter',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'enter',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      [
+        'enter',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      [
+        'leave',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      [
+        'leave',
+        'NonNullAssertion',
+        'nullabilityAssertion',
+        'ListNullabilityOperator',
+      ],
+      [
+        'leave',
+        'ListNullabilityOperator',
+        'nullabilityAssertion',
+        'NonNullAssertion',
+      ],
+      ['leave', 'NonNullAssertion', 'nullabilityAssertion', 'Field'],
+      ['leave', 'Field', 11, undefined],
       ['leave', 'SelectionSet', 'selectionSet', 'InlineFragment'],
       ['leave', 'InlineFragment', 1, undefined],
       ['enter', 'InlineFragment', 2, undefined],
diff --git a/src/type/__tests__/assertName-test.ts b/src/type/__tests__/assertName-test.ts
index 268b1c6e..1a281188 100644
--- a/src/type/__tests__/assertName-test.ts
+++ b/src/type/__tests__/assertName-test.ts
@@ -8,11 +8,6 @@ describe('assertName', () => {
     expect(assertName('_ValidName123')).to.equal('_ValidName123');
   });
 
-  it('throws for non-strings', () => {
-    // @ts-expect-error
-    expect(() => assertName({})).to.throw('Expected name to be a string.');
-  });
-
   it('throws on empty strings', () => {
     expect(() => assertName('')).to.throw(
       'Expected name to be a non-empty string.',
diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts
index 19d48291..7e891444 100644
--- a/src/type/__tests__/definition-test.ts
+++ b/src/type/__tests__/definition-test.ts
@@ -92,24 +92,6 @@ describe('Type System: Scalars', () => {
     ).to.equal('parseValue: { foo: { bar: "baz" } }');
   });
 
-  it('rejects a Scalar type without name', () => {
-    // @ts-expect-error
-    expect(() => new GraphQLScalarType({})).to.throw('Must provide name.');
-  });
-
-  it('rejects a Scalar type defining serialize with an incorrect type', () => {
-    expect(
-      () =>
-        new GraphQLScalarType({
-          name: 'SomeScalar',
-          // @ts-expect-error
-          serialize: {},
-        }),
-    ).to.throw(
-      'SomeScalar must provide "serialize" function. If this custom Scalar is also used as an input type, ensure "parseValue" and "parseLiteral" functions are also provided.',
-    );
-  });
-
   it('rejects a Scalar type defining parseLiteral but not parseValue', () => {
     expect(
       () =>
@@ -121,34 +103,6 @@ describe('Type System: Scalars', () => {
       'SomeScalar must provide both "parseValue" and "parseLiteral" functions.',
     );
   });
-
-  it('rejects a Scalar type defining parseValue and parseLiteral with an incorrect type', () => {
-    expect(
-      () =>
-        new GraphQLScalarType({
-          name: 'SomeScalar',
-          // @ts-expect-error
-          parseValue: {},
-          // @ts-expect-error
-          parseLiteral: {},
-        }),
-    ).to.throw(
-      'SomeScalar must provide both "parseValue" and "parseLiteral" functions.',
-    );
-  });
-
-  it('rejects a Scalar type defining specifiedByURL with an incorrect type', () => {
-    expect(
-      () =>
-        new GraphQLScalarType({
-          name: 'SomeScalar',
-          // @ts-expect-error
-          specifiedByURL: {},
-        }),
-    ).to.throw(
-      'SomeScalar must provide "specifiedByURL" as a string, but got: {}.',
-    );
-  });
 });
 
 describe('Type System: Objects', () => {
@@ -328,30 +282,6 @@ describe('Type System: Objects', () => {
     ).to.throw('Names must only contain [_a-zA-Z0-9] but "bad-name" does not.');
   });
 
-  it('rejects an Object type field with undefined config', () => {
-    const objType = new GraphQLObjectType({
-      name: 'SomeObject',
-      fields: {
-        // @ts-expect-error (must not be undefined)
-        f: undefined,
-      },
-    });
-    expect(() => objType.getFields()).to.throw(
-      'SomeObject.f field config must be an object.',
-    );
-  });
-
-  it('rejects an Object type with incorrectly typed fields', () => {
-    const objType = new GraphQLObjectType({
-      name: 'SomeObject',
-      // @ts-expect-error
-      fields: [{ field: ScalarType }],
-    });
-    expect(() => objType.getFields()).to.throw(
-      'SomeObject fields must be an object with field names as keys or a function which returns such an object.',
-    );
-  });
-
   it('rejects an Object type with incorrectly named fields', () => {
     const objType = new GraphQLObjectType({
       name: 'SomeObject',
@@ -375,22 +305,6 @@ describe('Type System: Objects', () => {
     expect(() => objType.getFields()).to.throw();
   });
 
-  it('rejects an Object type with incorrectly typed field args', () => {
-    const objType = new GraphQLObjectType({
-      name: 'SomeObject',
-      fields: {
-        badField: {
-          type: ScalarType,
-          // @ts-expect-error
-          args: [{ badArg: ScalarType }],
-        },
-      },
-    });
-    expect(() => objType.getFields()).to.throw(
-      'SomeObject.badField args must be an object with argument names as keys.',
-    );
-  });
-
   it('rejects an Object type with incorrectly named field args', () => {
     const objType = new GraphQLObjectType({
       name: 'SomeObject',
@@ -407,74 +321,6 @@ describe('Type System: Objects', () => {
       'Names must only contain [_a-zA-Z0-9] but "bad-name" does not.',
     );
   });
-
-  it('rejects an Object type with incorrectly typed interfaces', () => {
-    const objType = new GraphQLObjectType({
-      name: 'SomeObject',
-      fields: {},
-      // @ts-expect-error
-      interfaces: {},
-    });
-    expect(() => objType.getInterfaces()).to.throw(
-      'SomeObject interfaces must be an Array or a function which returns an Array.',
-    );
-  });
-
-  it('rejects an Object type with interfaces as a function returning an incorrect type', () => {
-    const objType = new GraphQLObjectType({
-      name: 'SomeObject',
-      fields: {},
-      // @ts-expect-error (Expected interfaces to return array)
-      interfaces() {
-        return {};
-      },
-    });
-    expect(() => objType.getInterfaces()).to.throw(
-      'SomeObject interfaces must be an Array or a function which returns an Array.',
-    );
-  });
-
-  it('rejects an empty Object field resolver', () => {
-    const objType = new GraphQLObjectType({
-      name: 'SomeObject',
-      fields: {
-        // @ts-expect-error (Expected resolve to be a function)
-        field: { type: ScalarType, resolve: {} },
-      },
-    });
-
-    expect(() => objType.getFields()).to.throw(
-      'SomeObject.field field resolver must be a function if provided, but got: {}.',
-    );
-  });
-
-  it('rejects a constant scalar value resolver', () => {
-    const objType = new GraphQLObjectType({
-      name: 'SomeObject',
-      fields: {
-        // @ts-expect-error (Expected resolve to be a function)
-        field: { type: ScalarType, resolve: 0 },
-      },
-    });
-
-    expect(() => objType.getFields()).to.throw(
-      'SomeObject.field field resolver must be a function if provided, but got: 0.',
-    );
-  });
-
-  it('rejects an Object type with an incorrect type for isTypeOf', () => {
-    expect(
-      () =>
-        new GraphQLObjectType({
-          name: 'AnotherObject',
-          fields: {},
-          // @ts-expect-error
-          isTypeOf: {},
-        }),
-    ).to.throw(
-      'AnotherObject must provide "isTypeOf" as a function, but got: {}.',
-    );
-  });
 });
 
 describe('Type System: Interfaces', () => {
@@ -511,46 +357,6 @@ describe('Type System: Interfaces', () => {
       () => new GraphQLInterfaceType({ name: 'bad-name', fields: {} }),
     ).to.throw('Names must only contain [_a-zA-Z0-9] but "bad-name" does not.');
   });
-
-  it('rejects an Interface type with incorrectly typed interfaces', () => {
-    const objType = new GraphQLInterfaceType({
-      name: 'AnotherInterface',
-      fields: {},
-      // @ts-expect-error
-      interfaces: {},
-    });
-    expect(() => objType.getInterfaces()).to.throw(
-      'AnotherInterface interfaces must be an Array or a function which returns an Array.',
-    );
-  });
-
-  it('rejects an Interface type with interfaces as a function returning an incorrect type', () => {
-    const objType = new GraphQLInterfaceType({
-      name: 'AnotherInterface',
-      fields: {},
-      // @ts-expect-error (Expected Array return)
-      interfaces() {
-        return {};
-      },
-    });
-    expect(() => objType.getInterfaces()).to.throw(
-      'AnotherInterface interfaces must be an Array or a function which returns an Array.',
-    );
-  });
-
-  it('rejects an Interface type with an incorrect type for resolveType', () => {
-    expect(
-      () =>
-        new GraphQLInterfaceType({
-          name: 'AnotherInterface',
-          fields: {},
-          // @ts-expect-error
-          resolveType: {},
-        }),
-    ).to.throw(
-      'AnotherInterface must provide "resolveType" as a function, but got: {}.',
-    );
-  });
 });
 
 describe('Type System: Unions', () => {
@@ -593,32 +399,6 @@ describe('Type System: Unions', () => {
       () => new GraphQLUnionType({ name: 'bad-name', types: [] }),
     ).to.throw('Names must only contain [_a-zA-Z0-9] but "bad-name" does not.');
   });
-
-  it('rejects an Union type with an incorrect type for resolveType', () => {
-    expect(
-      () =>
-        new GraphQLUnionType({
-          name: 'SomeUnion',
-          types: [],
-          // @ts-expect-error
-          resolveType: {},
-        }),
-    ).to.throw(
-      'SomeUnion must provide "resolveType" as a function, but got: {}.',
-    );
-  });
-
-  it('rejects a Union type with incorrectly typed types', () => {
-    const unionType = new GraphQLUnionType({
-      name: 'SomeUnion',
-      // @ts-expect-error
-      types: { ObjectType },
-    });
-
-    expect(() => unionType.getTypes()).to.throw(
-      'Must provide Array of types or a function which returns such an array for Union SomeUnion.',
-    );
-  });
 });
 
 describe('Type System: Enums', () => {
@@ -710,17 +490,6 @@ describe('Type System: Enums', () => {
     ).to.throw('Names must only contain [_a-zA-Z0-9] but "bad-name" does not.');
   });
 
-  it('rejects an Enum type with incorrectly typed values', () => {
-    expect(
-      () =>
-        new GraphQLEnumType({
-          name: 'SomeEnum',
-          // @ts-expect-error
-          values: [{ FOO: 10 }],
-        }),
-    ).to.throw('SomeEnum values must be an object with value names as keys.');
-  });
-
   it('rejects an Enum type with incorrectly named values', () => {
     expect(
       () =>
@@ -732,32 +501,6 @@ describe('Type System: Enums', () => {
         }),
     ).to.throw('Names must only contain [_a-zA-Z0-9] but "bad-name" does not.');
   });
-
-  it('rejects an Enum type with missing value definition', () => {
-    expect(
-      () =>
-        new GraphQLEnumType({
-          name: 'SomeEnum',
-          // @ts-expect-error (must not be null)
-          values: { FOO: null },
-        }),
-    ).to.throw(
-      'SomeEnum.FOO must refer to an object with a "value" key representing an internal value but got: null.',
-    );
-  });
-
-  it('rejects an Enum type with incorrectly typed value definition', () => {
-    expect(
-      () =>
-        new GraphQLEnumType({
-          name: 'SomeEnum',
-          // @ts-expect-error
-          values: { FOO: 10 },
-        }),
-    ).to.throw(
-      'SomeEnum.FOO must refer to an object with a "value" key representing an internal value but got: 10.',
-    );
-  });
 });
 
 describe('Type System: Input Objects', () => {
@@ -810,28 +553,6 @@ describe('Type System: Input Objects', () => {
       );
     });
 
-    it('rejects an Input Object type with incorrect fields', () => {
-      const inputObjType = new GraphQLInputObjectType({
-        name: 'SomeInputObject',
-        // @ts-expect-error
-        fields: [],
-      });
-      expect(() => inputObjType.getFields()).to.throw(
-        'SomeInputObject fields must be an object with field names as keys or a function which returns such an object.',
-      );
-    });
-
-    it('rejects an Input Object type with fields function that returns incorrect type', () => {
-      const inputObjType = new GraphQLInputObjectType({
-        name: 'SomeInputObject',
-        // @ts-expect-error
-        fields: () => [],
-      });
-      expect(() => inputObjType.getFields()).to.throw(
-        'SomeInputObject fields must be an object with field names as keys or a function which returns such an object.',
-      );
-    });
-
     it('rejects an Input Object type with incorrectly named fields', () => {
       const inputObjType = new GraphQLInputObjectType({
         name: 'SomeInputObject',
@@ -905,19 +626,6 @@ describe('Type System: List', () => {
     expectList(ListOfScalarsType).to.not.throw();
     expectList(NonNullScalarType).to.not.throw();
   });
-
-  it('rejects a non-type as item type of list', () => {
-    // @ts-expect-error
-    expectList({}).to.throw('Expected {} to be a GraphQL type.');
-    // @ts-expect-error
-    expectList(String).to.throw(
-      'Expected [function String] to be a GraphQL type.',
-    );
-    // @ts-expect-error (must provide type)
-    expectList(null).to.throw('Expected null to be a GraphQL type.');
-    // @ts-expect-error (must provide type)
-    expectList(undefined).to.throw('Expected undefined to be a GraphQL type.');
-  });
 });
 
 describe('Type System: Non-Null', () => {
@@ -935,26 +643,6 @@ describe('Type System: Non-Null', () => {
     expectNonNull(ListOfScalarsType).to.not.throw();
     expectNonNull(ListOfNonNullScalarsType).to.not.throw();
   });
-
-  it('rejects a non-type as nullable type of non-null', () => {
-    expectNonNull(NonNullScalarType).to.throw(
-      'Expected Scalar! to be a GraphQL nullable type.',
-    );
-    // @ts-expect-error
-    expectNonNull({}).to.throw('Expected {} to be a GraphQL nullable type.');
-    // @ts-expect-error
-    expectNonNull(String).to.throw(
-      'Expected [function String] to be a GraphQL nullable type.',
-    );
-    // @ts-expect-error (must provide type)
-    expectNonNull(null).to.throw(
-      'Expected null to be a GraphQL nullable type.',
-    );
-    // @ts-expect-error (must provide type)
-    expectNonNull(undefined).to.throw(
-      'Expected undefined to be a GraphQL nullable type.',
-    );
-  });
 });
 
 describe('Type System: test utility methods', () => {
diff --git a/src/type/__tests__/directive-test.ts b/src/type/__tests__/directive-test.ts
index 110a3cc9..7513282d 100644
--- a/src/type/__tests__/directive-test.ts
+++ b/src/type/__tests__/directive-test.ts
@@ -96,18 +96,6 @@ describe('Type System: Directive', () => {
     ).to.throw('Names must only contain [_a-zA-Z0-9] but "bad-name" does not.');
   });
 
-  it('rejects a directive with incorrectly typed args', () => {
-    expect(
-      () =>
-        new GraphQLDirective({
-          name: 'Foo',
-          locations: [DirectiveLocation.QUERY],
-          // @ts-expect-error
-          args: [],
-        }),
-    ).to.throw('@Foo args must be an object with argument names as keys.');
-  });
-
   it('rejects a directive with incorrectly named arg', () => {
     expect(
       () =>
@@ -120,18 +108,4 @@ describe('Type System: Directive', () => {
         }),
     ).to.throw('Names must only contain [_a-zA-Z0-9] but "bad-name" does not.');
   });
-
-  it('rejects a directive with undefined locations', () => {
-    // @ts-expect-error
-    expect(() => new GraphQLDirective({ name: 'Foo' })).to.throw(
-      '@Foo locations must be an Array.',
-    );
-  });
-
-  it('rejects a directive with incorrectly typed locations', () => {
-    // @ts-expect-error
-    expect(() => new GraphQLDirective({ name: 'Foo', locations: {} })).to.throw(
-      '@Foo locations must be an Array.',
-    );
-  });
 });
diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts
index 81e721e7..9a1692ea 100644
--- a/src/type/__tests__/predicate-test.ts
+++ b/src/type/__tests__/predicate-test.ts
@@ -73,6 +73,7 @@ import {
   GraphQLString,
   isSpecifiedScalarType,
 } from '../scalars';
+import { assertSchema, GraphQLSchema, isSchema } from '../schema';
 
 const ObjectType = new GraphQLObjectType({ name: 'Object', fields: {} });
 const InterfaceType = new GraphQLInterfaceType({
@@ -702,3 +703,31 @@ describe('Directive predicates', () => {
     });
   });
 });
+
+describe('Schema predicates', () => {
+  const schema = new GraphQLSchema({});
+
+  describe('isSchema/assertSchema', () => {
+    it('returns true for schema', () => {
+      expect(isSchema(schema)).to.equal(true);
+      expect(() => assertSchema(schema)).to.not.throw();
+    });
+
+    it('returns false for schema class (rather than instance)', () => {
+      expect(isSchema(GraphQLSchema)).to.equal(false);
+      expect(() => assertSchema(GraphQLSchema)).to.throw();
+    });
+
+    it('returns false for non-schema', () => {
+      expect(isSchema(EnumType)).to.equal(false);
+      expect(() => assertSchema(EnumType)).to.throw();
+      expect(isSchema(ScalarType)).to.equal(false);
+      expect(() => assertSchema(ScalarType)).to.throw();
+    });
+
+    it('returns false for random garbage', () => {
+      expect(isSchema({ what: 'is this' })).to.equal(false);
+      expect(() => assertSchema({ what: 'is this' })).to.throw();
+    });
+  });
+});
diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts
index 652bfeea..4c30bd4d 100644
--- a/src/type/__tests__/schema-test.ts
+++ b/src/type/__tests__/schema-test.ts
@@ -438,15 +438,6 @@ describe('Type System: Schema', () => {
           }).__validationErrors,
         ).to.equal(undefined);
       });
-
-      it('checks the configuration for mistakes', () => {
-        // @ts-expect-error
-        expect(() => new GraphQLSchema(JSON.parse)).to.throw();
-        // @ts-expect-error
-        expect(() => new GraphQLSchema({ types: {} })).to.throw();
-        // @ts-expect-error
-        expect(() => new GraphQLSchema({ directives: {} })).to.throw();
-      });
     });
 
     describe('A Schema must contain uniquely named types', () => {
@@ -466,19 +457,6 @@ describe('Type System: Schema', () => {
         );
       });
 
-      it('rejects a Schema when a provided type has no name', () => {
-        const query = new GraphQLObjectType({
-          name: 'Query',
-          fields: { foo: { type: GraphQLString } },
-        });
-        const types = [{}, query, {}];
-
-        // @ts-expect-error
-        expect(() => new GraphQLSchema({ query, types })).to.throw(
-          'One of the provided types for building the Schema is missing a name.',
-        );
-      });
-
       it('rejects a Schema which defines an object type twice', () => {
         const types = [
           new GraphQLObjectType({ name: 'SameName', fields: {} }),
diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts
index 32658c13..435abc2d 100644
--- a/src/utilities/__tests__/buildASTSchema-test.ts
+++ b/src/utilities/__tests__/buildASTSchema-test.ts
@@ -1092,16 +1092,4 @@ describe('Schema Builder', () => {
       'Unknown type: "UnknownType".',
     );
   });
-
-  it('Rejects invalid AST', () => {
-    // @ts-expect-error (First parameter expected to be DocumentNode)
-    expect(() => buildASTSchema(null)).to.throw(
-      'Must provide valid Document AST',
-    );
-
-    // @ts-expect-error
-    expect(() => buildASTSchema({})).to.throw(
-      'Must provide valid Document AST',
-    );
-  });
 });
diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts
index c275c023..66d8a00b 100644
--- a/src/utilities/__tests__/extendSchema-test.ts
+++ b/src/utilities/__tests__/extendSchema-test.ts
@@ -1167,20 +1167,6 @@ describe('extendSchema', () => {
     );
   });
 
-  it('Rejects invalid AST', () => {
-    const schema = new GraphQLSchema({});
-
-    // @ts-expect-error (Second argument expects DocumentNode)
-    expect(() => extendSchema(schema, null)).to.throw(
-      'Must provide valid Document AST',
-    );
-
-    // @ts-expect-error
-    expect(() => extendSchema(schema, {})).to.throw(
-      'Must provide valid Document AST',
-    );
-  });
-
   it('does not allow replacing a default directive', () => {
     const schema = new GraphQLSchema({});
     const extendAST = parse(`
diff --git a/src/utilities/__tests__/stripIgnoredCharacters-test.ts b/src/utilities/__tests__/stripIgnoredCharacters-test.ts
index caf6529b..a6c8bc41 100644
--- a/src/utilities/__tests__/stripIgnoredCharacters-test.ts
+++ b/src/utilities/__tests__/stripIgnoredCharacters-test.ts
@@ -208,8 +208,14 @@ describe('stripIgnoredCharacters', () => {
     const strippedQuery = stripIgnoredCharacters(kitchenSinkQuery);
     expect(stripIgnoredCharacters(strippedQuery)).to.equal(strippedQuery);
 
-    const queryAST = parse(kitchenSinkQuery, { noLocation: true });
-    const strippedAST = parse(strippedQuery, { noLocation: true });
+    const queryAST = parse(kitchenSinkQuery, {
+      noLocation: true,
+      experimentalClientControlledNullability: true,
+    });
+    const strippedAST = parse(strippedQuery, {
+      noLocation: true,
+      experimentalClientControlledNullability: true,
+    });
     expect(strippedAST).to.deep.equal(queryAST);
   });
 
diff --git a/src/validation/__tests__/validation-test.ts b/src/validation/__tests__/validation-test.ts
index 2d49f933..45e2307a 100644
--- a/src/validation/__tests__/validation-test.ts
+++ b/src/validation/__tests__/validation-test.ts
@@ -17,11 +17,6 @@ import type { ValidationContext } from '../ValidationContext';
 import { testSchema } from './harness';
 
 describe('Validate: Supports full validation', () => {
-  it('rejects invalid documents', () => {
-    // @ts-expect-error (expects a DocumentNode as a second parameter)
-    expect(() => validate(testSchema, null)).to.throw('Must provide document.');
-  });
-
   it('validates queries', () => {
     const doc = parse(`
       query {