Skip to content

Commit bac9ad6

Browse files
committed
introduce completePromiseCatchingErrors helpers
use it within executeField, completeListItemValue, executeStreamField this takes advantage of some quirks involving promises/await
1 parent 500b458 commit bac9ad6

File tree

3 files changed

+100
-88
lines changed

3 files changed

+100
-88
lines changed

src/execution/__tests__/nonnull-test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,16 @@ describe('Execute: handles non-nullable types', () => {
259259
path: ['syncNest', 'syncNest', 'sync'],
260260
locations: [{ line: 6, column: 22 }],
261261
},
262+
{
263+
message: promiseError.message,
264+
path: ['syncNest', 'promise'],
265+
locations: [{ line: 5, column: 11 }],
266+
},
267+
{
268+
message: promiseError.message,
269+
path: ['syncNest', 'syncNest', 'promise'],
270+
locations: [{ line: 6, column: 27 }],
271+
},
262272
{
263273
message: syncError.message,
264274
path: ['syncNest', 'promiseNest', 'sync'],
@@ -274,21 +284,6 @@ describe('Execute: handles non-nullable types', () => {
274284
path: ['promiseNest', 'syncNest', 'sync'],
275285
locations: [{ line: 12, column: 22 }],
276286
},
277-
{
278-
message: promiseError.message,
279-
path: ['syncNest', 'promise'],
280-
locations: [{ line: 5, column: 11 }],
281-
},
282-
{
283-
message: promiseError.message,
284-
path: ['syncNest', 'syncNest', 'promise'],
285-
locations: [{ line: 6, column: 27 }],
286-
},
287-
{
288-
message: syncError.message,
289-
path: ['promiseNest', 'promiseNest', 'sync'],
290-
locations: [{ line: 13, column: 25 }],
291-
},
292287
{
293288
message: promiseError.message,
294289
path: ['syncNest', 'promiseNest', 'promise'],
@@ -304,6 +299,11 @@ describe('Execute: handles non-nullable types', () => {
304299
path: ['promiseNest', 'syncNest', 'promise'],
305300
locations: [{ line: 12, column: 27 }],
306301
},
302+
{
303+
message: syncError.message,
304+
path: ['promiseNest', 'promiseNest', 'sync'],
305+
locations: [{ line: 13, column: 25 }],
306+
},
307307
{
308308
message: promiseError.message,
309309
path: ['promiseNest', 'promiseNest', 'promise'],

src/execution/__tests__/stream-test.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,9 @@ describe('Execute: stream directive', () => {
11741174
],
11751175
},
11761176
],
1177+
hasNext: true,
1178+
},
1179+
{
11771180
hasNext: false,
11781181
},
11791182
]);
@@ -1197,19 +1200,25 @@ describe('Execute: stream directive', () => {
11971200
} /* c8 ignore stop */,
11981201
},
11991202
});
1200-
expectJSON(result).toDeepEqual({
1201-
errors: [
1202-
{
1203-
message:
1204-
'Cannot return null for non-nullable field NestedObject.nonNullScalarField.',
1205-
locations: [{ line: 4, column: 11 }],
1206-
path: ['nestedObject', 'nonNullScalarField'],
1203+
expectJSON(result).toDeepEqual([
1204+
{
1205+
errors: [
1206+
{
1207+
message:
1208+
'Cannot return null for non-nullable field NestedObject.nonNullScalarField.',
1209+
locations: [{ line: 4, column: 11 }],
1210+
path: ['nestedObject', 'nonNullScalarField'],
1211+
},
1212+
],
1213+
data: {
1214+
nestedObject: null,
12071215
},
1208-
],
1209-
data: {
1210-
nestedObject: null,
1216+
hasNext: true,
12111217
},
1212-
});
1218+
{
1219+
hasNext: false,
1220+
},
1221+
]);
12131222
});
12141223
it('Filters payloads that are nulled by a later synchronous error', async () => {
12151224
const document = parse(`
@@ -1350,6 +1359,9 @@ describe('Execute: stream directive', () => {
13501359
],
13511360
},
13521361
],
1362+
hasNext: true,
1363+
},
1364+
{
13531365
hasNext: false,
13541366
},
13551367
]);

src/execution/execute.ts

Lines changed: 62 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -715,25 +715,15 @@ function executeField(
715715
const result = resolveFn(source, args, contextValue, info);
716716

717717
if (isPromise(result)) {
718-
const completed = result.then((resolved) =>
719-
completeValue(
720-
exeContext,
721-
returnType,
722-
fieldNodes,
723-
info,
724-
path,
725-
resolved,
726-
asyncPayloadRecord,
727-
),
718+
return completePromiseCatchingErrors(
719+
exeContext,
720+
returnType,
721+
fieldNodes,
722+
info,
723+
path,
724+
result,
725+
asyncPayloadRecord,
728726
);
729-
// Note: we don't rely on a `catch` method, but we do expect "thenable"
730-
// to take a second callback for the error case.
731-
return completed.then(undefined, (rawError) => {
732-
const error = locatedError(rawError, fieldNodes, pathToArray(path));
733-
const handledError = handleFieldError(error, returnType, errors);
734-
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
735-
return handledError;
736-
});
737727
}
738728

739729
const completed = completeValue(
@@ -922,6 +912,41 @@ function completeValue(
922912
);
923913
}
924914

915+
async function completePromiseCatchingErrors(
916+
exeContext: ExecutionContext,
917+
returnType: GraphQLOutputType,
918+
fieldNodes: ReadonlyArray<FieldNode>,
919+
info: GraphQLResolveInfo,
920+
path: Path,
921+
result: Promise<unknown>,
922+
asyncPayloadRecord?: AsyncPayloadRecord,
923+
): Promise<unknown> {
924+
try {
925+
const resolved = await result;
926+
let completed = completeValue(
927+
exeContext,
928+
returnType,
929+
fieldNodes,
930+
info,
931+
path,
932+
resolved,
933+
asyncPayloadRecord,
934+
);
935+
if (isPromise(completed)) {
936+
// see: https://github.com/tc39/proposal-faster-promise-adoption
937+
// it is faster to await a promise prior to returning it from an async function
938+
completed = await completed;
939+
}
940+
return completed;
941+
} catch (rawError) {
942+
const errors = asyncPayloadRecord?.errors ?? exeContext.errors;
943+
const error = locatedError(rawError, fieldNodes, pathToArray(path));
944+
const handledError = handleFieldError(error, returnType, errors);
945+
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
946+
return handledError;
947+
}
948+
}
949+
925950
/**
926951
* Returns an object containing the `@stream` arguments if a field should be
927952
* streamed based on the experimental flag, stream directive present and
@@ -1156,29 +1181,18 @@ function completeListItemValue(
11561181
asyncPayloadRecord?: AsyncPayloadRecord,
11571182
): boolean {
11581183
if (isPromise(item)) {
1159-
const completedItem = item.then((resolved) =>
1160-
completeValue(
1184+
completedResults.push(
1185+
completePromiseCatchingErrors(
11611186
exeContext,
11621187
itemType,
11631188
fieldNodes,
11641189
info,
11651190
itemPath,
1166-
resolved,
1191+
item,
11671192
asyncPayloadRecord,
11681193
),
11691194
);
11701195

1171-
// Note: we don't rely on a `catch` method, but we do expect "thenable"
1172-
// to take a second callback for the error case.
1173-
completedResults.push(
1174-
completedItem.then(undefined, (rawError) => {
1175-
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
1176-
const handledError = handleFieldError(error, itemType, errors);
1177-
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
1178-
return handledError;
1179-
}),
1180-
);
1181-
11821196
return true;
11831197
}
11841198

@@ -1897,36 +1911,22 @@ function executeStreamField(
18971911
exeContext,
18981912
});
18991913
if (isPromise(item)) {
1900-
const completedItems = item
1901-
.then((resolved) =>
1902-
completeValue(
1903-
exeContext,
1904-
itemType,
1905-
fieldNodes,
1906-
info,
1907-
itemPath,
1908-
resolved,
1909-
asyncPayloadRecord,
1910-
),
1911-
)
1912-
.then(undefined, (rawError) => {
1913-
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
1914-
const handledError = handleFieldError(
1915-
error,
1916-
itemType,
1917-
asyncPayloadRecord.errors,
1918-
);
1919-
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
1920-
return handledError;
1921-
})
1922-
.then(
1923-
(value) => [value],
1924-
(error) => {
1925-
asyncPayloadRecord.errors.push(error);
1926-
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
1927-
return null;
1928-
},
1929-
);
1914+
const completedItems = completePromiseCatchingErrors(
1915+
exeContext,
1916+
itemType,
1917+
fieldNodes,
1918+
info,
1919+
itemPath,
1920+
item,
1921+
asyncPayloadRecord,
1922+
).then(
1923+
(value) => [value],
1924+
(error) => {
1925+
asyncPayloadRecord.errors.push(error);
1926+
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
1927+
return null;
1928+
},
1929+
);
19301930

19311931
asyncPayloadRecord.addItems(completedItems);
19321932
return asyncPayloadRecord;

0 commit comments

Comments
 (0)