Skip to content

Commit 4104cd8

Browse files
committed
Fix crash in node when mixing sync/async resolvers
1 parent 9a494d9 commit 4104cd8

File tree

2 files changed

+73
-13
lines changed

2 files changed

+73
-13
lines changed

src/execution/__tests__/executor-test.ts

+50
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assert, expect } from 'chai';
22
import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON';
5+
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick';
56

67
import { inspect } from '../../jsutils/inspect';
78

@@ -576,6 +577,55 @@ describe('Execute: Handles basic execution tasks', () => {
576577
});
577578
});
578579

580+
it('handles sync errors combined with rejections', async () => {
581+
let isAsyncResolverCalled = false;
582+
583+
const schema = new GraphQLSchema({
584+
query: new GraphQLObjectType({
585+
name: 'Query',
586+
fields: {
587+
syncNullError: {
588+
type: new GraphQLNonNull(GraphQLString),
589+
resolve: () => null,
590+
},
591+
asyncNullError: {
592+
type: new GraphQLNonNull(GraphQLString),
593+
async resolve() {
594+
await resolveOnNextTick();
595+
await resolveOnNextTick();
596+
await resolveOnNextTick();
597+
isAsyncResolverCalled = true;
598+
return Promise.resolve(null);
599+
},
600+
},
601+
},
602+
}),
603+
});
604+
605+
// Order is important here, as the promise has to be created before the synchronous error is thrown
606+
const document = parse(`
607+
{
608+
asyncNullError
609+
syncNullError
610+
}
611+
`);
612+
613+
const result = await execute({ schema, document });
614+
615+
expect(isAsyncResolverCalled).to.equal(true);
616+
expectJSON(result).toDeepEqual({
617+
data: null,
618+
errors: [
619+
{
620+
message:
621+
'Cannot return null for non-nullable field Query.syncNullError.',
622+
locations: [{ line: 4, column: 9 }],
623+
path: ['syncNullError'],
624+
},
625+
],
626+
});
627+
});
628+
579629
it('Full response path is included for non-nullable fields', () => {
580630
const A: GraphQLObjectType = new GraphQLObjectType({
581631
name: 'A',

src/execution/execute.ts

+23-13
Original file line numberDiff line numberDiff line change
@@ -431,22 +431,32 @@ function executeFields(
431431
const results = Object.create(null);
432432
let containsPromise = false;
433433

434-
for (const [responseName, fieldNodes] of fields) {
435-
const fieldPath = addPath(path, responseName, parentType.name);
436-
const result = executeField(
437-
exeContext,
438-
parentType,
439-
sourceValue,
440-
fieldNodes,
441-
fieldPath,
442-
);
434+
try {
435+
for (const [responseName, fieldNodes] of fields) {
436+
const fieldPath = addPath(path, responseName, parentType.name);
437+
const result = executeField(
438+
exeContext,
439+
parentType,
440+
sourceValue,
441+
fieldNodes,
442+
fieldPath,
443+
);
443444

444-
if (result !== undefined) {
445-
results[responseName] = result;
446-
if (isPromise(result)) {
447-
containsPromise = true;
445+
if (result !== undefined) {
446+
results[responseName] = result;
447+
if (isPromise(result)) {
448+
containsPromise = true;
449+
}
448450
}
449451
}
452+
} catch (error) {
453+
if (containsPromise) {
454+
// Ensure that any promises returned by other fields are handled, as they may also reject.
455+
return promiseForObject(results).finally(() => {
456+
throw error;
457+
});
458+
}
459+
throw error;
450460
}
451461

452462
// If there are no promises, we can just return the object

0 commit comments

Comments
 (0)