Skip to content

Commit f62c0a2

Browse files
committed
Resolvers can return Error to signify failure
This adds another way resolver functions can signify a failure: returning an Error object. The primary reason this is useful is returning a list of values where index contains an error but another does not. A test casehas been added to illustrate this behavior. Previously sync resolvers could only throw an error, killing the whole list, or return nulls and fail to present error messaging. I then used this as a simplifying factor to the executor core where it removed a third instance of locating and logging an error.
1 parent 8c52207 commit f62c0a2

File tree

2 files changed

+83
-30
lines changed

2 files changed

+83
-30
lines changed

src/execution/__tests__/executor.js

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,18 @@ describe('Execute: Handles basic execution tasks', () => {
246246

247247
it('nulls out error subtrees', async () => {
248248
var doc = `{
249-
sync,
250-
syncError,
251-
syncRawError,
252-
async,
253-
asyncReject,
254-
asyncRawReject,
255-
asyncEmptyReject,
256-
asyncError,
249+
sync
250+
syncError
251+
syncRawError
252+
syncReturnError
253+
syncReturnErrorList
254+
async
255+
asyncReject
256+
asyncRawReject
257+
asyncEmptyReject
258+
asyncError
257259
asyncRawError
260+
asyncReturnError
258261
}`;
259262

260263
var data = {
@@ -269,6 +272,17 @@ describe('Execute: Handles basic execution tasks', () => {
269272
throw 'Error getting syncRawError';
270273
/* eslint-enable */
271274
},
275+
syncReturnError() {
276+
return new Error('Error getting syncReturnError');
277+
},
278+
syncReturnErrorList() {
279+
return [
280+
'sync0',
281+
new Error('Error getting syncReturnErrorList1'),
282+
'sync2',
283+
new Error('Error getting syncReturnErrorList3')
284+
];
285+
},
272286
async() {
273287
return new Promise(resolve => resolve('async'));
274288
},
@@ -294,7 +308,10 @@ describe('Execute: Handles basic execution tasks', () => {
294308
throw 'Error getting asyncRawError';
295309
/* eslint-enable */
296310
});
297-
}
311+
},
312+
asyncReturnError() {
313+
return Promise.resolve(new Error('Error getting asyncReturnError'));
314+
},
298315
};
299316

300317
let ast = parse(doc);
@@ -305,12 +322,15 @@ describe('Execute: Handles basic execution tasks', () => {
305322
sync: { type: GraphQLString },
306323
syncError: { type: GraphQLString },
307324
syncRawError: { type: GraphQLString },
325+
syncReturnError: { type: GraphQLString },
326+
syncReturnErrorList: { type: new GraphQLList(GraphQLString) },
308327
async: { type: GraphQLString },
309328
asyncReject: { type: GraphQLString },
310329
asyncRawReject: { type: GraphQLString },
311330
asyncEmptyReject: { type: GraphQLString },
312331
asyncError: { type: GraphQLString },
313332
asyncRawError: { type: GraphQLString },
333+
asyncReturnError: { type: GraphQLString },
314334
}
315335
})
316336
});
@@ -321,29 +341,40 @@ describe('Execute: Handles basic execution tasks', () => {
321341
sync: 'sync',
322342
syncError: null,
323343
syncRawError: null,
344+
syncReturnError: null,
345+
syncReturnErrorList: [ 'sync0', null, 'sync2', null ],
324346
async: 'async',
325347
asyncReject: null,
326348
asyncRawReject: null,
327349
asyncEmptyReject: null,
328350
asyncError: null,
329351
asyncRawError: null,
352+
asyncReturnError: null,
330353
});
331354

332355
expect(result.errors && result.errors.map(formatError)).to.deep.equal([
333356
{ message: 'Error getting syncError',
334357
locations: [ { line: 3, column: 7 } ] },
335358
{ message: 'Error getting syncRawError',
336359
locations: [ { line: 4, column: 7 } ] },
337-
{ message: 'Error getting asyncReject',
360+
{ message: 'Error getting syncReturnError',
361+
locations: [ { line: 5, column: 7 } ] },
362+
{ message: 'Error getting syncReturnErrorList1',
363+
locations: [ { line: 6, column: 7 } ] },
364+
{ message: 'Error getting syncReturnErrorList3',
338365
locations: [ { line: 6, column: 7 } ] },
366+
{ message: 'Error getting asyncReturnError',
367+
locations: [ { line: 13, column: 7 } ] },
368+
{ message: 'Error getting asyncReject',
369+
locations: [ { line: 8, column: 7 } ] },
339370
{ message: 'Error getting asyncRawReject',
340-
locations: [ { line: 7, column: 7 } ] },
371+
locations: [ { line: 9, column: 7 } ] },
341372
{ message: 'An unknown error occurred.',
342-
locations: [ { line: 8, column: 7 } ] },
373+
locations: [ { line: 10, column: 7 } ] },
343374
{ message: 'Error getting asyncError',
344-
locations: [ { line: 9, column: 7 } ] },
375+
locations: [ { line: 11, column: 7 } ] },
345376
{ message: 'Error getting asyncRawError',
346-
locations: [ { line: 10, column: 7 } ] },
377+
locations: [ { line: 12, column: 7 } ] },
347378
]);
348379
});
349380

src/execution/execute.js

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -508,20 +508,9 @@ function resolveField(
508508
variableValues: exeContext.variableValues,
509509
};
510510

511-
// If an error occurs while calling the field `resolve` function, ensure that
512-
// it is wrapped as a GraphQLError with locations. Log this error and return
513-
// null if allowed, otherwise throw the error so the parent field can handle
514-
// it.
515-
try {
516-
var result = resolveFn(source, args, info);
517-
} catch (error) {
518-
var reportedError = locatedError(error, fieldASTs);
519-
if (returnType instanceof GraphQLNonNull) {
520-
throw reportedError;
521-
}
522-
exeContext.errors.push(reportedError);
523-
return null;
524-
}
511+
// Get the resolve function, regardless of if it's result is normal
512+
// or abrupt (error).
513+
var result = resolveOrError(resolveFn, source, args, info);
525514

526515
return completeValueCatchingError(
527516
exeContext,
@@ -532,6 +521,29 @@ function resolveField(
532521
);
533522
}
534523

524+
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
525+
// function. Returns the result of resolveFn or the abrupt-return Error object.
526+
function resolveOrError<T>(
527+
resolveFn: (
528+
source: any,
529+
args: { [key: string]: any },
530+
info: GraphQLResolveInfo
531+
) => T,
532+
source: any,
533+
args: { [key: string]: any },
534+
info: GraphQLResolveInfo
535+
): Error | T {
536+
try {
537+
return resolveFn(source, args, info);
538+
} catch (error) {
539+
// Sometimes a non-error is thrown, wrap it as an Error for a
540+
// consistent interface.
541+
return error instanceof Error ? error : new Error(error);
542+
}
543+
}
544+
545+
// This is a small wrapper around completeValue which detects and logs errors
546+
// in the execution context.
535547
function completeValueCatchingError(
536548
exeContext: ExecutionContext,
537549
returnType: GraphQLType,
@@ -556,6 +568,8 @@ function completeValueCatchingError(
556568
result
557569
);
558570
if (isThenable(completed)) {
571+
// If `completeValue` returned a rejected promise, log the rejection
572+
// error and resolve to null.
559573
// Note: we don't rely on a `catch` method, but we do expect "thenable"
560574
// to take a second callback for the error case.
561575
return completed.then(undefined, error => {
@@ -565,6 +579,8 @@ function completeValueCatchingError(
565579
}
566580
return completed;
567581
} catch (error) {
582+
// If `completeValue` returned abruptly (threw an error), log the error
583+
// and return null.
568584
exeContext.errors.push(error);
569585
return null;
570586
}
@@ -595,21 +611,27 @@ function completeValue(
595611
info: GraphQLResolveInfo,
596612
result: any
597613
): any {
598-
// If result is a Promise, resolve it, if the Promise is rejected, construct
599-
// a GraphQLError with proper locations.
614+
// If result is a Promise, apply-lift over completeValue.
600615
if (isThenable(result)) {
601616
return result.then(
617+
// Once resolved to a value, complete that value.
602618
resolved => completeValue(
603619
exeContext,
604620
returnType,
605621
fieldASTs,
606622
info,
607623
resolved
608624
),
625+
// If rejected, create a located error, and continue to reject.
609626
error => Promise.reject(locatedError(error, fieldASTs))
610627
);
611628
}
612629

630+
// If result is an Error, throw a located error.
631+
if (result instanceof Error) {
632+
throw locatedError(result, fieldASTs);
633+
}
634+
613635
// If field type is NonNull, complete for inner type, and throw field error
614636
// if result is null.
615637
if (returnType instanceof GraphQLNonNull) {

0 commit comments

Comments
 (0)