diff --git a/src/execution/__tests__/errorPropagation-test.ts b/src/execution/__tests__/errorPropagation-test.ts new file mode 100644 index 0000000000..a935ddced8 --- /dev/null +++ b/src/execution/__tests__/errorPropagation-test.ts @@ -0,0 +1,74 @@ +import { describe, it } from 'mocha'; + +import { expectJSON } from '../../__testUtils__/expectJSON.js'; + +import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js'; + +import { parse } from '../../language/parser.js'; + +import { buildSchema } from '../../utilities/buildASTSchema.js'; + +import { execute } from '../execute.js'; +import type { ExecutionResult } from '../types.js'; + +const syncError = new Error('bar'); + +const throwingData = { + foo() { + throw syncError; + }, +}; + +const schema = buildSchema(` + type Query { + foo : Int! + } + + directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION +`); + +function executeQuery( + query: string, + rootValue: unknown, +): PromiseOrValue { + return execute({ schema, document: parse(query), rootValue }); +} + +describe('Execute: handles errors', () => { + it('with `@experimental_disableErrorPropagation returns null', async () => { + const query = ` + query getFoo @experimental_disableErrorPropagation { + foo + } + `; + const result = await executeQuery(query, throwingData); + expectJSON(result).toDeepEqual({ + data: { foo: null }, + errors: [ + { + message: 'bar', + path: ['foo'], + locations: [{ line: 3, column: 9 }], + }, + ], + }); + }); + it('without `experimental_disableErrorPropagation` propagates the error', async () => { + const query = ` + query getFoo { + foo + } + `; + const result = await executeQuery(query, throwingData); + expectJSON(result).toDeepEqual({ + data: null, + errors: [ + { + message: 'bar', + path: ['foo'], + locations: [{ line: 3, column: 9 }], + }, + ], + }); + }); +}); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 6fd3fc5b4a..71cbdb6b42 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -44,7 +44,10 @@ import { isNonNullType, isObjectType, } from '../type/definition.js'; -import { GraphQLStreamDirective } from '../type/directives.js'; +import { + GraphQLDisableErrorPropagationDirective, + GraphQLStreamDirective, +} from '../type/directives.js'; import type { GraphQLSchema } from '../type/schema.js'; import { assertValidSchema } from '../type/validate.js'; @@ -170,6 +173,7 @@ export interface ExecutionContext { abortSignalListener: AbortSignalListener | undefined; completed: boolean; cancellableStreams: Set | undefined; + errorPropagation: boolean; } interface IncrementalContext { @@ -314,6 +318,15 @@ export function executeQueryOrMutationOrSubscriptionEvent( return ensureSinglePayload(result); } +function errorPropagation(operation: OperationDefinitionNode): boolean { + const directiveNode = operation.directives?.find( + (directive) => + directive.name.value === GraphQLDisableErrorPropagationDirective.name, + ); + + return directiveNode === undefined; +} + export function experimentalExecuteQueryOrMutationOrSubscriptionEvent( validatedExecutionArgs: ValidatedExecutionArgs, ): PromiseOrValue { @@ -326,6 +339,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent( : undefined, completed: false, cancellableStreams: undefined, + errorPropagation: errorPropagation(validatedExecutionArgs.operation), }; try { const { @@ -976,7 +990,7 @@ function handleFieldError( // If the field type is non-nullable, then it is resolved without any // protection from errors, however it still properly locates the error. - if (isNonNullType(returnType)) { + if (exeContext.errorPropagation && isNonNullType(returnType)) { throw error; } diff --git a/src/type/directives.ts b/src/type/directives.ts index 6b6f5d4fa2..96f5b6b65a 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -276,6 +276,19 @@ export const GraphQLOneOfDirective: GraphQLDirective = new GraphQLDirective({ args: {}, }); +/** + * Disables error propagation (experimental). + */ +export const GraphQLDisableErrorPropagationDirective = new GraphQLDirective({ + name: 'experimental_disableErrorPropagation', + description: 'Disables error propagation.', + locations: [ + DirectiveLocation.QUERY, + DirectiveLocation.MUTATION, + DirectiveLocation.SUBSCRIPTION, + ], +}); + /** * The full list of specified directives. */