Skip to content

Commit e4f2ace

Browse files
committed
feat: introduce GraphQLAggregateError
1 parent 25db102 commit e4f2ace

File tree

9 files changed

+278
-19
lines changed

9 files changed

+278
-19
lines changed

docs/APIReference-Errors.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: graphql/error
33
layout: ../_core/GraphQLJSLayout
44
category: API Reference
55
permalink: /graphql-js/error/
6-
sublinks: formatError,GraphQLError,locatedError,syntaxError
6+
sublinks: formatError,GraphQLError,locatedError,syntaxError,GraphQLAggregateError
77
next: /graphql-js/execution/
88
---
99

@@ -107,3 +107,20 @@ type GraphQLErrorLocation = {
107107
108108
Given a GraphQLError, format it according to the rules described by the
109109
Response Format, Errors section of the GraphQL Specification.
110+
111+
### GraphQLAggregateError
112+
113+
```js
114+
class GraphQLAggregateError extends Error {
115+
constructor(
116+
errors: Array<Error>,
117+
message?: string
118+
)
119+
}
120+
```
121+
122+
A helper class for bundling multiple distinct errors. When a
123+
GraphQLAggregateError is thrown during execution of a GraphQL operation,
124+
a GraphQLError will be produced from each individual errors and will be
125+
reported separately, according to the rules described by the Response
126+
Format, Errors section of the GraphQL Specification.

src/error/GraphQLAggregateError.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* A GraphQLAggregateError is a container for multiple errors.
3+
*
4+
* This helper can be used to report multiple distinct errors simultaneously.
5+
* Note that error handlers must be aware aggregated errors may be reported so as to
6+
* properly handle the contained errors.
7+
*
8+
* See also:
9+
* https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-aggregate-error-objects
10+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
11+
* https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.aggregate-error.js
12+
* https://github.com/sindresorhus/aggregate-error
13+
*
14+
*/
15+
export class GraphQLAggregateError<T = Error> extends Error {
16+
readonly errors!: ReadonlyArray<T>;
17+
18+
constructor(errors: ReadonlyArray<T>, message?: string) {
19+
super(message);
20+
21+
Object.defineProperties(this, {
22+
name: { value: 'GraphQLAggregateError' },
23+
message: {
24+
value: message,
25+
writable: true,
26+
},
27+
errors: {
28+
value: errors,
29+
},
30+
});
31+
}
32+
33+
get [Symbol.toStringTag](): string {
34+
return 'GraphQLAggregateError';
35+
}
36+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { GraphQLAggregateError } from '../GraphQLAggregateError';
5+
6+
describe('GraphQLAggregateError', () => {
7+
it('is a class and is a subclass of Error', () => {
8+
const errors = [new Error('Error1'), new Error('Error2')];
9+
expect(new GraphQLAggregateError(errors)).to.be.instanceof(Error);
10+
expect(new GraphQLAggregateError(errors)).to.be.instanceof(
11+
GraphQLAggregateError,
12+
);
13+
});
14+
15+
it('has a name, errors, and a message (if provided)', () => {
16+
const errors = [new Error('Error1'), new Error('Error2')];
17+
const e = new GraphQLAggregateError(errors, 'msg');
18+
19+
expect(e).to.include({
20+
name: 'GraphQLAggregateError',
21+
errors,
22+
message: 'msg',
23+
});
24+
});
25+
});

src/error/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { GraphQLAggregateError } from './GraphQLAggregateError';
12
export { GraphQLError, printError, formatError } from './GraphQLError';
23
export type { GraphQLFormattedError } from './GraphQLError';
34

src/execution/__tests__/executor-test.ts

Lines changed: 116 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { invariant } from '../../jsutils/invariant';
99
import { Kind } from '../../language/kinds';
1010
import { parse } from '../../language/parser';
1111

12+
import { GraphQLAggregateError } from '../../error/GraphQLAggregateError';
1213
import { GraphQLSchema } from '../../type/schema';
1314
import { GraphQLInt, GraphQLBoolean, GraphQLString } from '../../type/scalars';
1415
import {
@@ -403,17 +404,22 @@ describe('Execute: Handles basic execution tasks', () => {
403404
fields: {
404405
sync: { type: GraphQLString },
405406
syncError: { type: GraphQLString },
407+
syncAggregateError: { type: GraphQLString },
406408
syncRawError: { type: GraphQLString },
407409
syncReturnError: { type: GraphQLString },
410+
syncReturnAggregateError: { type: GraphQLString },
408411
syncReturnErrorList: { type: new GraphQLList(GraphQLString) },
409412
async: { type: GraphQLString },
410413
asyncReject: { type: GraphQLString },
414+
asyncRejectAggregate: { type: GraphQLString },
411415
asyncRejectWithExtensions: { type: GraphQLString },
412416
asyncRawReject: { type: GraphQLString },
413417
asyncEmptyReject: { type: GraphQLString },
414418
asyncError: { type: GraphQLString },
419+
asyncAggregateError: { type: GraphQLString },
415420
asyncRawError: { type: GraphQLString },
416421
asyncReturnError: { type: GraphQLString },
422+
asyncReturnAggregateError: { type: GraphQLString },
417423
asyncReturnErrorWithExtensions: { type: GraphQLString },
418424
},
419425
}),
@@ -423,16 +429,22 @@ describe('Execute: Handles basic execution tasks', () => {
423429
{
424430
sync
425431
syncError
432+
syncAggregateError
426433
syncRawError
427434
syncReturnError
435+
syncReturnAggregateError
428436
syncReturnErrorList
429437
async
430438
asyncReject
439+
asyncRejectAggregate
431440
asyncRawReject
441+
asyncRawRejectAggregate
432442
asyncEmptyReject
433443
asyncError
444+
asyncAggregateError
434445
asyncRawError
435446
asyncReturnError
447+
asyncReturnAggregateError
436448
asyncReturnErrorWithExtensions
437449
}
438450
`);
@@ -444,13 +456,25 @@ describe('Execute: Handles basic execution tasks', () => {
444456
syncError() {
445457
throw new Error('Error getting syncError');
446458
},
459+
syncAggregateError() {
460+
throw new GraphQLAggregateError([
461+
new Error('Error1 getting syncAggregateError'),
462+
new Error('Error2 getting syncAggregateError'),
463+
]);
464+
},
447465
syncRawError() {
448466
// eslint-disable-next-line @typescript-eslint/no-throw-literal
449467
throw 'Error getting syncRawError';
450468
},
451469
syncReturnError() {
452470
return new Error('Error getting syncReturnError');
453471
},
472+
syncReturnAggregateError() {
473+
return new GraphQLAggregateError([
474+
new Error('Error1 getting syncReturnAggregateError'),
475+
new Error('Error2 getting syncReturnAggregateError'),
476+
]);
477+
},
454478
syncReturnErrorList() {
455479
return [
456480
'sync0',
@@ -467,6 +491,16 @@ describe('Execute: Handles basic execution tasks', () => {
467491
reject(new Error('Error getting asyncReject')),
468492
);
469493
},
494+
asyncRejectAggregate() {
495+
return new Promise((_, reject) =>
496+
reject(
497+
new GraphQLAggregateError([
498+
new Error('Error1 getting asyncRejectAggregate'),
499+
new Error('Error2 getting asyncRejectAggregate'),
500+
]),
501+
),
502+
);
503+
},
470504
asyncRawReject() {
471505
// eslint-disable-next-line prefer-promise-reject-errors
472506
return Promise.reject('Error getting asyncRawReject');
@@ -480,6 +514,14 @@ describe('Execute: Handles basic execution tasks', () => {
480514
throw new Error('Error getting asyncError');
481515
});
482516
},
517+
asyncAggregateError() {
518+
return new Promise(() => {
519+
throw new GraphQLAggregateError([
520+
new Error('Error1 getting asyncAggregateError'),
521+
new Error('Error2 getting asyncAggregateError'),
522+
]);
523+
});
524+
},
483525
asyncRawError() {
484526
return new Promise(() => {
485527
// eslint-disable-next-line @typescript-eslint/no-throw-literal
@@ -489,6 +531,14 @@ describe('Execute: Handles basic execution tasks', () => {
489531
asyncReturnError() {
490532
return Promise.resolve(new Error('Error getting asyncReturnError'));
491533
},
534+
asyncReturnAggregateError() {
535+
return Promise.resolve(
536+
new GraphQLAggregateError([
537+
new Error('Error1 getting asyncReturnAggregateError'),
538+
new Error('Error2 getting asyncReturnAggregateError'),
539+
]),
540+
);
541+
},
492542
asyncReturnErrorWithExtensions() {
493543
const error = new Error('Error getting asyncReturnErrorWithExtensions');
494544
// @ts-expect-error
@@ -503,16 +553,21 @@ describe('Execute: Handles basic execution tasks', () => {
503553
data: {
504554
sync: 'sync',
505555
syncError: null,
556+
syncAggregateError: null,
506557
syncRawError: null,
507558
syncReturnError: null,
559+
syncReturnAggregateError: null,
508560
syncReturnErrorList: ['sync0', null, 'sync2', null],
509561
async: 'async',
510562
asyncReject: null,
563+
asyncRejectAggregate: null,
511564
asyncRawReject: null,
512565
asyncEmptyReject: null,
513566
asyncError: null,
567+
asyncAggregateError: null,
514568
asyncRawError: null,
515569
asyncReturnError: null,
570+
asyncReturnAggregateError: null,
516571
asyncReturnErrorWithExtensions: null,
517572
},
518573
errors: [
@@ -522,58 +577,108 @@ describe('Execute: Handles basic execution tasks', () => {
522577
path: ['syncError'],
523578
},
524579
{
525-
message: 'Unexpected error value: "Error getting syncRawError"',
580+
message: 'Error1 getting syncAggregateError',
581+
locations: [{ line: 5, column: 9 }],
582+
path: ['syncAggregateError'],
583+
},
584+
{
585+
message: 'Error2 getting syncAggregateError',
526586
locations: [{ line: 5, column: 9 }],
587+
path: ['syncAggregateError'],
588+
},
589+
{
590+
message: 'Unexpected error value: "Error getting syncRawError"',
591+
locations: [{ line: 6, column: 9 }],
527592
path: ['syncRawError'],
528593
},
529594
{
530595
message: 'Error getting syncReturnError',
531-
locations: [{ line: 6, column: 9 }],
596+
locations: [{ line: 7, column: 9 }],
532597
path: ['syncReturnError'],
533598
},
599+
{
600+
message: 'Error1 getting syncReturnAggregateError',
601+
locations: [{ line: 8, column: 9 }],
602+
path: ['syncReturnAggregateError'],
603+
},
604+
{
605+
message: 'Error2 getting syncReturnAggregateError',
606+
locations: [{ line: 8, column: 9 }],
607+
path: ['syncReturnAggregateError'],
608+
},
534609
{
535610
message: 'Error getting syncReturnErrorList1',
536-
locations: [{ line: 7, column: 9 }],
611+
locations: [{ line: 9, column: 9 }],
537612
path: ['syncReturnErrorList', 1],
538613
},
539614
{
540615
message: 'Error getting syncReturnErrorList3',
541-
locations: [{ line: 7, column: 9 }],
616+
locations: [{ line: 9, column: 9 }],
542617
path: ['syncReturnErrorList', 3],
543618
},
544619
{
545620
message: 'Error getting asyncReject',
546-
locations: [{ line: 9, column: 9 }],
621+
locations: [{ line: 11, column: 9 }],
547622
path: ['asyncReject'],
548623
},
624+
{
625+
message: 'Error1 getting asyncRejectAggregate',
626+
locations: [{ line: 12, column: 9 }],
627+
path: ['asyncRejectAggregate'],
628+
},
629+
{
630+
message: 'Error2 getting asyncRejectAggregate',
631+
locations: [{ line: 12, column: 9 }],
632+
path: ['asyncRejectAggregate'],
633+
},
549634
{
550635
message: 'Unexpected error value: "Error getting asyncRawReject"',
551-
locations: [{ line: 10, column: 9 }],
636+
locations: [{ line: 13, column: 9 }],
552637
path: ['asyncRawReject'],
553638
},
554639
{
555640
message: 'Unexpected error value: undefined',
556-
locations: [{ line: 11, column: 9 }],
641+
locations: [{ line: 15, column: 9 }],
557642
path: ['asyncEmptyReject'],
558643
},
559644
{
560645
message: 'Error getting asyncError',
561-
locations: [{ line: 12, column: 9 }],
646+
locations: [{ line: 16, column: 9 }],
562647
path: ['asyncError'],
563648
},
649+
{
650+
message: 'Error1 getting asyncAggregateError',
651+
locations: [{ line: 17, column: 9 }],
652+
path: ['asyncAggregateError'],
653+
},
654+
{
655+
message: 'Error2 getting asyncAggregateError',
656+
locations: [{ line: 17, column: 9 }],
657+
path: ['asyncAggregateError'],
658+
},
564659
{
565660
message: 'Unexpected error value: "Error getting asyncRawError"',
566-
locations: [{ line: 13, column: 9 }],
661+
locations: [{ line: 18, column: 9 }],
567662
path: ['asyncRawError'],
568663
},
569664
{
570665
message: 'Error getting asyncReturnError',
571-
locations: [{ line: 14, column: 9 }],
666+
locations: [{ line: 19, column: 9 }],
572667
path: ['asyncReturnError'],
573668
},
669+
{
670+
message: 'Error1 getting asyncReturnAggregateError',
671+
locations: [{ line: 20, column: 9 }],
672+
path: ['asyncReturnAggregateError'],
673+
},
674+
{
675+
message: 'Error2 getting asyncReturnAggregateError',
676+
locations: [{ line: 20, column: 9 }],
677+
path: ['asyncReturnAggregateError'],
678+
},
574679
{
575680
message: 'Error getting asyncReturnErrorWithExtensions',
576-
locations: [{ line: 15, column: 9 }],
681+
locations: [{ line: 21, column: 9 }],
577682
path: ['asyncReturnErrorWithExtensions'],
578683
extensions: { foo: 'bar' },
579684
},

0 commit comments

Comments
 (0)