Skip to content

Commit f66639a

Browse files
committed
feat: introduce GraphQLAggregateError
1 parent 32fda3b commit f66639a

File tree

9 files changed

+279
-19
lines changed

9 files changed

+279
-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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export { GraphQLAggregateError } from './GraphQLAggregateError';
2+
13
export { GraphQLError, printError } from './GraphQLError';
24

35
export { syntaxError } from './syntaxError';

src/execution/__tests__/executor-test.ts

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

10+
import { GraphQLAggregateError } from '../../error/GraphQLAggregateError';
1011
import { GraphQLSchema } from '../../type/schema';
1112
import { GraphQLInt, GraphQLBoolean, GraphQLString } from '../../type/scalars';
1213
import {
@@ -401,17 +402,22 @@ describe('Execute: Handles basic execution tasks', () => {
401402
fields: {
402403
sync: { type: GraphQLString },
403404
syncError: { type: GraphQLString },
405+
syncAggregateError: { type: GraphQLString },
404406
syncRawError: { type: GraphQLString },
405407
syncReturnError: { type: GraphQLString },
408+
syncReturnAggregateError: { type: GraphQLString },
406409
syncReturnErrorList: { type: new GraphQLList(GraphQLString) },
407410
async: { type: GraphQLString },
408411
asyncReject: { type: GraphQLString },
412+
asyncRejectAggregate: { type: GraphQLString },
409413
asyncRejectWithExtensions: { type: GraphQLString },
410414
asyncRawReject: { type: GraphQLString },
411415
asyncEmptyReject: { type: GraphQLString },
412416
asyncError: { type: GraphQLString },
417+
asyncAggregateError: { type: GraphQLString },
413418
asyncRawError: { type: GraphQLString },
414419
asyncReturnError: { type: GraphQLString },
420+
asyncReturnAggregateError: { type: GraphQLString },
415421
asyncReturnErrorWithExtensions: { type: GraphQLString },
416422
},
417423
}),
@@ -421,16 +427,22 @@ describe('Execute: Handles basic execution tasks', () => {
421427
{
422428
sync
423429
syncError
430+
syncAggregateError
424431
syncRawError
425432
syncReturnError
433+
syncReturnAggregateError
426434
syncReturnErrorList
427435
async
428436
asyncReject
437+
asyncRejectAggregate
429438
asyncRawReject
439+
asyncRawRejectAggregate
430440
asyncEmptyReject
431441
asyncError
442+
asyncAggregateError
432443
asyncRawError
433444
asyncReturnError
445+
asyncReturnAggregateError
434446
asyncReturnErrorWithExtensions
435447
}
436448
`);
@@ -442,13 +454,25 @@ describe('Execute: Handles basic execution tasks', () => {
442454
syncError() {
443455
throw new Error('Error getting syncError');
444456
},
457+
syncAggregateError() {
458+
throw new GraphQLAggregateError([
459+
new Error('Error1 getting syncAggregateError'),
460+
new Error('Error2 getting syncAggregateError'),
461+
]);
462+
},
445463
syncRawError() {
446464
// eslint-disable-next-line @typescript-eslint/no-throw-literal
447465
throw 'Error getting syncRawError';
448466
},
449467
syncReturnError() {
450468
return new Error('Error getting syncReturnError');
451469
},
470+
syncReturnAggregateError() {
471+
return new GraphQLAggregateError([
472+
new Error('Error1 getting syncReturnAggregateError'),
473+
new Error('Error2 getting syncReturnAggregateError'),
474+
]);
475+
},
452476
syncReturnErrorList() {
453477
return [
454478
'sync0',
@@ -465,6 +489,16 @@ describe('Execute: Handles basic execution tasks', () => {
465489
reject(new Error('Error getting asyncReject')),
466490
);
467491
},
492+
asyncRejectAggregate() {
493+
return new Promise((_, reject) =>
494+
reject(
495+
new GraphQLAggregateError([
496+
new Error('Error1 getting asyncRejectAggregate'),
497+
new Error('Error2 getting asyncRejectAggregate'),
498+
]),
499+
),
500+
);
501+
},
468502
asyncRawReject() {
469503
// eslint-disable-next-line prefer-promise-reject-errors
470504
return Promise.reject('Error getting asyncRawReject');
@@ -478,6 +512,14 @@ describe('Execute: Handles basic execution tasks', () => {
478512
throw new Error('Error getting asyncError');
479513
});
480514
},
515+
asyncAggregateError() {
516+
return new Promise(() => {
517+
throw new GraphQLAggregateError([
518+
new Error('Error1 getting asyncAggregateError'),
519+
new Error('Error2 getting asyncAggregateError'),
520+
]);
521+
});
522+
},
481523
asyncRawError() {
482524
return new Promise(() => {
483525
// eslint-disable-next-line @typescript-eslint/no-throw-literal
@@ -487,6 +529,14 @@ describe('Execute: Handles basic execution tasks', () => {
487529
asyncReturnError() {
488530
return Promise.resolve(new Error('Error getting asyncReturnError'));
489531
},
532+
asyncReturnAggregateError() {
533+
return Promise.resolve(
534+
new GraphQLAggregateError([
535+
new Error('Error1 getting asyncReturnAggregateError'),
536+
new Error('Error2 getting asyncReturnAggregateError'),
537+
]),
538+
);
539+
},
490540
asyncReturnErrorWithExtensions() {
491541
const error = new Error('Error getting asyncReturnErrorWithExtensions');
492542
// @ts-expect-error
@@ -501,16 +551,21 @@ describe('Execute: Handles basic execution tasks', () => {
501551
data: {
502552
sync: 'sync',
503553
syncError: null,
554+
syncAggregateError: null,
504555
syncRawError: null,
505556
syncReturnError: null,
557+
syncReturnAggregateError: null,
506558
syncReturnErrorList: ['sync0', null, 'sync2', null],
507559
async: 'async',
508560
asyncReject: null,
561+
asyncRejectAggregate: null,
509562
asyncRawReject: null,
510563
asyncEmptyReject: null,
511564
asyncError: null,
565+
asyncAggregateError: null,
512566
asyncRawError: null,
513567
asyncReturnError: null,
568+
asyncReturnAggregateError: null,
514569
asyncReturnErrorWithExtensions: null,
515570
},
516571
errors: [
@@ -520,58 +575,108 @@ describe('Execute: Handles basic execution tasks', () => {
520575
path: ['syncError'],
521576
},
522577
{
523-
message: 'Unexpected error value: "Error getting syncRawError"',
578+
message: 'Error1 getting syncAggregateError',
579+
locations: [{ line: 5, column: 9 }],
580+
path: ['syncAggregateError'],
581+
},
582+
{
583+
message: 'Error2 getting syncAggregateError',
524584
locations: [{ line: 5, column: 9 }],
585+
path: ['syncAggregateError'],
586+
},
587+
{
588+
message: 'Unexpected error value: "Error getting syncRawError"',
589+
locations: [{ line: 6, column: 9 }],
525590
path: ['syncRawError'],
526591
},
527592
{
528593
message: 'Error getting syncReturnError',
529-
locations: [{ line: 6, column: 9 }],
594+
locations: [{ line: 7, column: 9 }],
530595
path: ['syncReturnError'],
531596
},
597+
{
598+
message: 'Error1 getting syncReturnAggregateError',
599+
locations: [{ line: 8, column: 9 }],
600+
path: ['syncReturnAggregateError'],
601+
},
602+
{
603+
message: 'Error2 getting syncReturnAggregateError',
604+
locations: [{ line: 8, column: 9 }],
605+
path: ['syncReturnAggregateError'],
606+
},
532607
{
533608
message: 'Error getting syncReturnErrorList1',
534-
locations: [{ line: 7, column: 9 }],
609+
locations: [{ line: 9, column: 9 }],
535610
path: ['syncReturnErrorList', 1],
536611
},
537612
{
538613
message: 'Error getting syncReturnErrorList3',
539-
locations: [{ line: 7, column: 9 }],
614+
locations: [{ line: 9, column: 9 }],
540615
path: ['syncReturnErrorList', 3],
541616
},
542617
{
543618
message: 'Error getting asyncReject',
544-
locations: [{ line: 9, column: 9 }],
619+
locations: [{ line: 11, column: 9 }],
545620
path: ['asyncReject'],
546621
},
622+
{
623+
message: 'Error1 getting asyncRejectAggregate',
624+
locations: [{ line: 12, column: 9 }],
625+
path: ['asyncRejectAggregate'],
626+
},
627+
{
628+
message: 'Error2 getting asyncRejectAggregate',
629+
locations: [{ line: 12, column: 9 }],
630+
path: ['asyncRejectAggregate'],
631+
},
547632
{
548633
message: 'Unexpected error value: "Error getting asyncRawReject"',
549-
locations: [{ line: 10, column: 9 }],
634+
locations: [{ line: 13, column: 9 }],
550635
path: ['asyncRawReject'],
551636
},
552637
{
553638
message: 'Unexpected error value: undefined',
554-
locations: [{ line: 11, column: 9 }],
639+
locations: [{ line: 15, column: 9 }],
555640
path: ['asyncEmptyReject'],
556641
},
557642
{
558643
message: 'Error getting asyncError',
559-
locations: [{ line: 12, column: 9 }],
644+
locations: [{ line: 16, column: 9 }],
560645
path: ['asyncError'],
561646
},
647+
{
648+
message: 'Error1 getting asyncAggregateError',
649+
locations: [{ line: 17, column: 9 }],
650+
path: ['asyncAggregateError'],
651+
},
652+
{
653+
message: 'Error2 getting asyncAggregateError',
654+
locations: [{ line: 17, column: 9 }],
655+
path: ['asyncAggregateError'],
656+
},
562657
{
563658
message: 'Unexpected error value: "Error getting asyncRawError"',
564-
locations: [{ line: 13, column: 9 }],
659+
locations: [{ line: 18, column: 9 }],
565660
path: ['asyncRawError'],
566661
},
567662
{
568663
message: 'Error getting asyncReturnError',
569-
locations: [{ line: 14, column: 9 }],
664+
locations: [{ line: 19, column: 9 }],
570665
path: ['asyncReturnError'],
571666
},
667+
{
668+
message: 'Error1 getting asyncReturnAggregateError',
669+
locations: [{ line: 20, column: 9 }],
670+
path: ['asyncReturnAggregateError'],
671+
},
672+
{
673+
message: 'Error2 getting asyncReturnAggregateError',
674+
locations: [{ line: 20, column: 9 }],
675+
path: ['asyncReturnAggregateError'],
676+
},
572677
{
573678
message: 'Error getting asyncReturnErrorWithExtensions',
574-
locations: [{ line: 15, column: 9 }],
679+
locations: [{ line: 21, column: 9 }],
575680
path: ['asyncReturnErrorWithExtensions'],
576681
extensions: { foo: 'bar' },
577682
},

0 commit comments

Comments
 (0)