Skip to content

Commit f3109c3

Browse files
committed
Implement onError proposal
1 parent 6b253e7 commit f3109c3

File tree

7 files changed

+71
-3
lines changed

7 files changed

+71
-3
lines changed

src/error/ErrorBehavior.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type GraphQLErrorBehavior = 'PROPAGATE' | 'NO_PROPAGATE' | 'ABORT';
2+
3+
export function isErrorBehavior(
4+
onError: unknown,
5+
): onError is GraphQLErrorBehavior {
6+
return (
7+
onError === 'PROPAGATE' || onError === 'NO_PROPAGATE' || onError === 'ABORT'
8+
);
9+
}

src/error/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export type {
99
export { syntaxError } from './syntaxError';
1010

1111
export { locatedError } from './locatedError';
12+
export type { GraphQLErrorBehavior } from './ErrorBehavior';

src/execution/__tests__/executor-test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ describe('Execute: Handles basic execution tasks', () => {
263263
'rootValue',
264264
'operation',
265265
'variableValues',
266+
'errorBehavior',
266267
);
267268

268269
const operation = document.definitions[0];
@@ -275,6 +276,7 @@ describe('Execute: Handles basic execution tasks', () => {
275276
schema,
276277
rootValue,
277278
operation,
279+
errorBehavior: 'PROPAGATE',
278280
});
279281

280282
const field = operation.selectionSet.selections[0];

src/execution/execute.ts

+42-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { promiseForObject } from '../jsutils/promiseForObject';
1313
import type { PromiseOrValue } from '../jsutils/PromiseOrValue';
1414
import { promiseReduce } from '../jsutils/promiseReduce';
1515

16+
import type { GraphQLErrorBehavior } from '../error/ErrorBehavior';
17+
import { isErrorBehavior } from '../error/ErrorBehavior';
1618
import type { GraphQLFormattedError } from '../error/GraphQLError';
1719
import { GraphQLError } from '../error/GraphQLError';
1820
import { locatedError } from '../error/locatedError';
@@ -115,6 +117,7 @@ export interface ExecutionContext {
115117
typeResolver: GraphQLTypeResolver<any, any>;
116118
subscribeFieldResolver: GraphQLFieldResolver<any, any>;
117119
errors: Array<GraphQLError>;
120+
errorBehavior: GraphQLErrorBehavior;
118121
}
119122

120123
/**
@@ -130,6 +133,7 @@ export interface ExecutionResult<
130133
> {
131134
errors?: ReadonlyArray<GraphQLError>;
132135
data?: TData | null;
136+
onError?: GraphQLErrorBehavior;
133137
extensions?: TExtensions;
134138
}
135139

@@ -152,6 +156,15 @@ export interface ExecutionArgs {
152156
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
153157
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
154158
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
159+
/**
160+
* Experimental. Set to NO_PROPAGATE to prevent error propagation. Set to ABORT to
161+
* abort a request when any error occurs.
162+
*
163+
* Default: PROPAGATE
164+
*
165+
* @experimental
166+
*/
167+
onError?: GraphQLErrorBehavior;
155168
}
156169

157170
/**
@@ -286,8 +299,17 @@ export function buildExecutionContext(
286299
fieldResolver,
287300
typeResolver,
288301
subscribeFieldResolver,
302+
onError,
289303
} = args;
290304

305+
if (onError != null && !isErrorBehavior(onError)) {
306+
return [
307+
new GraphQLError(
308+
'Unsupported `onError` value; supported values are `PROPAGATE`, `NO_PROPAGATE` and `ABORT`.',
309+
),
310+
];
311+
}
312+
291313
let operation: OperationDefinitionNode | undefined;
292314
const fragments: ObjMap<FragmentDefinitionNode> = Object.create(null);
293315
for (const definition of document.definitions) {
@@ -347,6 +369,7 @@ export function buildExecutionContext(
347369
typeResolver: typeResolver ?? defaultTypeResolver,
348370
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
349371
errors: [],
372+
errorBehavior: onError ?? 'PROPAGATE',
350373
};
351374
}
352375

@@ -585,6 +608,7 @@ export function buildResolveInfo(
585608
rootValue: exeContext.rootValue,
586609
operation: exeContext.operation,
587610
variableValues: exeContext.variableValues,
611+
errorBehavior: exeContext.errorBehavior,
588612
};
589613
}
590614

@@ -593,10 +617,25 @@ function handleFieldError(
593617
returnType: GraphQLOutputType,
594618
exeContext: ExecutionContext,
595619
): null {
596-
// If the field type is non-nullable, then it is resolved without any
597-
// protection from errors, however it still properly locates the error.
598-
if (isNonNullType(returnType)) {
620+
if (exeContext.errorBehavior === 'PROPAGATE') {
621+
// If the field type is non-nullable, then it is resolved without any
622+
// protection from errors, however it still properly locates the error.
623+
// Note: semantic non-null types are treated as nullable for the purposes
624+
// of error handling.
625+
if (isNonNullType(returnType)) {
626+
throw error;
627+
}
628+
} else if (exeContext.errorBehavior === 'ABORT') {
629+
// In this mode, any error aborts the request
599630
throw error;
631+
} else if (exeContext.errorBehavior === 'NO_PROPAGATE') {
632+
// In this mode, the client takes responsibility for error handling, so we
633+
// treat the field as if it were nullable.
634+
} else {
635+
invariant(
636+
false,
637+
'Unexpected errorBehavior setting: ' + inspect(exeContext.errorBehavior),
638+
);
600639
}
601640

602641
// Otherwise, error protection is applied, logging the error and resolving

src/graphql.ts

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { isPromise } from './jsutils/isPromise';
33
import type { Maybe } from './jsutils/Maybe';
44
import type { PromiseOrValue } from './jsutils/PromiseOrValue';
55

6+
import type { GraphQLErrorBehavior } from './error/ErrorBehavior';
7+
68
import { parse } from './language/parser';
79
import type { Source } from './language/source';
810

@@ -66,6 +68,15 @@ export interface GraphQLArgs {
6668
operationName?: Maybe<string>;
6769
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
6870
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
71+
/**
72+
* Experimental. Set to NO_PROPAGATE to prevent error propagation. Set to ABORT to
73+
* abort a request when any error occurs.
74+
*
75+
* Default: PROPAGATE
76+
*
77+
* @experimental
78+
*/
79+
onError?: GraphQLErrorBehavior;
6980
}
7081

7182
export function graphql(args: GraphQLArgs): Promise<ExecutionResult> {
@@ -106,6 +117,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
106117
operationName,
107118
fieldResolver,
108119
typeResolver,
120+
onError,
109121
} = args;
110122

111123
// Validate Schema
@@ -138,5 +150,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
138150
operationName,
139151
fieldResolver,
140152
typeResolver,
153+
onError,
141154
});
142155
}

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ export {
395395
} from './error/index';
396396

397397
export type {
398+
GraphQLErrorBehavior,
398399
GraphQLErrorOptions,
399400
GraphQLFormattedError,
400401
GraphQLErrorExtensions,

src/type/definition.ts

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { PromiseOrValue } from '../jsutils/PromiseOrValue';
1414
import { suggestionList } from '../jsutils/suggestionList';
1515
import { toObjMap } from '../jsutils/toObjMap';
1616

17+
import type { GraphQLErrorBehavior } from '../error/ErrorBehavior';
1718
import { GraphQLError } from '../error/GraphQLError';
1819

1920
import type {
@@ -988,6 +989,8 @@ export interface GraphQLResolveInfo {
988989
readonly rootValue: unknown;
989990
readonly operation: OperationDefinitionNode;
990991
readonly variableValues: { [variable: string]: unknown };
992+
/** @experimental */
993+
readonly errorBehavior: GraphQLErrorBehavior;
991994
}
992995

993996
/**

0 commit comments

Comments
 (0)