/** @category Errors */
import { isObjectLike } from '../jsutils/isObjectLike';
import type { Maybe } from '../jsutils/Maybe';
import type { ASTNode, Location } from '../language/ast';
import type { SourceLocation } from '../language/location';
import { getLocation } from '../language/location';
import { printLocation, printSourceLocation } from '../language/printLocation';
import type { Source } from '../language/source';
/**
* Custom extensions
* @remarks
* Use a unique identifier name for your extension, for example the name of
* your library or project. Do not use a shortened identifier as this increases
* the risk of conflicts. We recommend you add at most one extension field,
* an object which can contain all the values you need.
*/
export interface GraphQLErrorExtensions {
[attributeName: string]: unknown;
}
/**
* Custom formatted extensions
* @remarks
* Use a unique identifier name for your extension, for example the name of
* your library or project. Do not use a shortened identifier as this increases
* the risk of conflicts. We recommend you add at most one extension field,
* an object which can contain all the values you need.
*/
export interface GraphQLFormattedErrorExtensions {
[attributeName: string]: unknown;
}
/** Options used to construct a GraphQLError. */
export interface GraphQLErrorOptions {
/** AST node or nodes associated with this error. */
nodes?: ReadonlyArray<ASTNode> | ASTNode | null;
/** Source document used to derive error locations. */
source?: Maybe<Source>;
/** Character offsets in the source document associated with this error. */
positions?: Maybe<ReadonlyArray<number>>;
/** Response path where this error occurred during execution. */
path?: Maybe<ReadonlyArray<string | number>>;
/** Original error that caused this GraphQLError, if one exists. */
originalError?: Maybe<
Error & {
/** Extension fields associated with this value. */
readonly extensions?: unknown;
}
>;
/** Extension fields to include in the formatted result. */
extensions?: Maybe<GraphQLErrorExtensions>;
}
type BackwardsCompatibleArgs =
| [options?: GraphQLErrorOptions]
| [
nodes?: GraphQLErrorOptions['nodes'],
source?: GraphQLErrorOptions['source'],
positions?: GraphQLErrorOptions['positions'],
path?: GraphQLErrorOptions['path'],
originalError?: GraphQLErrorOptions['originalError'],
extensions?: GraphQLErrorOptions['extensions'],
];
function toNormalizedOptions(
args: BackwardsCompatibleArgs,
): GraphQLErrorOptions {
const firstArg = args[0];
if (firstArg == null || 'kind' in firstArg || 'length' in firstArg) {
return {
nodes: firstArg,
source: args[1],
positions: args[2],
path: args[3],
originalError: args[4],
extensions: args[5],
};
}
return firstArg;
}
/**
* A GraphQLError describes an Error found during the parse, validate, or
* execute phases of performing a GraphQL operation. In addition to a message
* and stack trace, it also includes information about the locations in a
* GraphQL document and/or execution result that correspond to the Error.
*/
export class GraphQLError extends Error {
/**
* An array of `{ line, column }` locations within the source GraphQL document
* which correspond to this error.
*
* Errors during validation often contain multiple locations, for example to
* point out two things with the same name. Errors during execution include a
* single location, the field which produced the error.
*
* Enumerable, and appears in the result of JSON.stringify().
*/
readonly locations: ReadonlyArray<SourceLocation> | undefined;
/**
* An array describing the JSON-path into the execution response which
* corresponds to this error. Only included for errors during execution.
*
* Enumerable, and appears in the result of JSON.stringify().
*/
readonly path: ReadonlyArray<string | number> | undefined;
/** An array of GraphQL AST Nodes corresponding to this error. */
readonly nodes: ReadonlyArray<ASTNode> | undefined;
/**
* The source GraphQL document for the first location of this error.
*
* Note that if this Error represents more than one node, the source may not
* represent nodes after the first node.
*/
readonly source: Source | undefined;
/**
* An array of character offsets within the source GraphQL document
* which correspond to this error.
*/
readonly positions: ReadonlyArray<number> | undefined;
/** Original error that caused this GraphQLError, if one exists. */
readonly originalError: Error | undefined;
/** Extension fields to add to the formatted error. */
readonly extensions: GraphQLErrorExtensions;
/**
* Creates a GraphQLError instance.
* @param message - Human-readable error message.
* @param options - Error metadata such as source locations, response path, original error, and extensions.
* This positional-arguments constructor overload is deprecated. Use the
* `GraphQLError(message, options)` overload instead.
* @example
* ```ts
* // Create an error from AST nodes and response metadata.
* import { parse } from 'graphql/language';
* import { GraphQLError } from 'graphql/error';
*
* const document = parse('{ greeting }');
* const fieldNode = document.definitions[0].selectionSet.selections[0];
* const error = new GraphQLError('Cannot query this field.', {
* nodes: fieldNode,
* path: ['greeting'],
* extensions: { code: 'FORBIDDEN' },
* });
*
* error.message; // => 'Cannot query this field.'
* error.locations; // => [{ line: 1, column: 3 }]
* error.path; // => ['greeting']
* error.extensions; // => { code: 'FORBIDDEN' }
* ```
* @example
* ```ts
* // This variant derives locations from source positions and preserves the original error.
* import { Source } from 'graphql/language';
* import { GraphQLError } from 'graphql/error';
*
* const source = new Source('{ greeting }');
* const originalError = new Error('Database unavailable.');
* const error = new GraphQLError('Resolver failed.', {
* source,
* positions: [2],
* path: ['greeting'],
* originalError,
* });
*
* error.locations; // => [{ line: 1, column: 3 }]
* error.path; // => ['greeting']
* error.originalError; // => originalError
* ```
*/
constructor(message: string, options?: GraphQLErrorOptions);
/**
* Creates a GraphQLError instance using the legacy positional constructor.
* This deprecated overload will be removed in v17. Prefer the
* `GraphQLErrorOptions` object overload, which keeps optional error metadata
* in a single options bag.
* @param message - Human-readable error message.
* @param nodes - AST node or nodes associated with this error.
* @param source - Source document used to derive error locations.
* @param positions - Character offsets in the source document associated with
* this error.
* @param path - Response path where this error occurred during execution.
* @param originalError - Original error that caused this GraphQLError, if one
* exists.
* @param extensions - Extension fields to include in the formatted error.
* @example
* ```ts
* import { Source } from 'graphql/language';
* import { GraphQLError } from 'graphql/error';
*
* const source = new Source('{ greeting }');
* const originalError = new Error('Database unavailable.');
* const error = new GraphQLError(
* 'Resolver failed.',
* undefined,
* source,
* [2],
* ['greeting'],
* originalError,
* { code: 'INTERNAL' },
* );
*
* error.locations; // => [{ line: 1, column: 3 }]
* error.path; // => ['greeting']
* error.originalError; // => originalError
* error.extensions; // => { code: 'INTERNAL' }
* ```
* @deprecated Please use the `GraphQLErrorOptions` constructor overload instead.
*/
constructor(
message: string,
nodes?: ReadonlyArray<ASTNode> | ASTNode | null,
source?: Maybe<Source>,
positions?: Maybe<ReadonlyArray<number>>,
path?: Maybe<ReadonlyArray<string | number>>,
originalError?: Maybe<Error & { readonly extensions?: unknown }>,
extensions?: Maybe<GraphQLErrorExtensions>,
);
constructor(message: string, ...rawArgs: BackwardsCompatibleArgs) {
const { nodes, source, positions, path, originalError, extensions } =
toNormalizedOptions(rawArgs);
super(message);
this.name = 'GraphQLError';
this.path = path ?? undefined;
this.originalError = originalError ?? undefined;
// Compute list of blame nodes.
this.nodes = undefinedIfEmpty(
Array.isArray(nodes) ? nodes : nodes ? [nodes] : undefined,
);
const nodeLocations = undefinedIfEmpty(
this.nodes
?.map((node) => node.loc)
.filter((loc): loc is Location => loc != null),
);
// Compute locations in the source for the given nodes/positions.
this.source = source ?? nodeLocations?.[0]?.source;
this.positions = positions ?? nodeLocations?.map((loc) => loc.start);
this.locations =
positions && source
? positions.map((pos) => getLocation(source, pos))
: nodeLocations?.map((loc) => getLocation(loc.source, loc.start));
const originalExtensions = isObjectLike(originalError?.extensions)
? originalError?.extensions
: undefined;
this.extensions = extensions ?? originalExtensions ?? Object.create(null);
// Only properties prescribed by the spec should be enumerable.
// Keep the rest as non-enumerable.
Object.defineProperties(this, {
message: {
writable: true,
enumerable: true,
},
name: { enumerable: false },
nodes: { enumerable: false },
source: { enumerable: false },
positions: { enumerable: false },
originalError: { enumerable: false },
});
// Include (non-enumerable) stack trace.
/* c8 ignore start */
// FIXME: https://github.com/graphql/graphql-js/issues/2317
if (originalError?.stack) {
Object.defineProperty(this, 'stack', {
value: originalError.stack,
writable: true,
configurable: true,
});
} else if (Error.captureStackTrace) {
Error.captureStackTrace(this, GraphQLError);
} else {
Object.defineProperty(this, 'stack', {
value: Error().stack,
writable: true,
configurable: true,
});
}
/* c8 ignore stop */
}
/**
* Returns the value used by `Object.prototype.toString`.
* @returns The built-in string tag for this object.
*/
get [Symbol.toStringTag](): string {
return 'GraphQLError';
}
/**
* Returns this error as a human-readable message with source locations.
* @returns The formatted error string.
* @example
* ```ts
* import { Source } from 'graphql/language';
* import { GraphQLError } from 'graphql/error';
*
* const error = new GraphQLError('Cannot query field "name".', {
* source: new Source('{ name }'),
* positions: [2],
* });
*
* error.toString(); // => 'Cannot query field "name".\n\nGraphQL request:1:3\n1 | { name }\n | ^'
* ```
*/
toString(): string {
let output = this.message;
if (this.nodes) {
for (const node of this.nodes) {
if (node.loc) {
output += '\n\n' + printLocation(node.loc);
}
}
} else if (this.source && this.locations) {
for (const location of this.locations) {
output += '\n\n' + printSourceLocation(this.source, location);
}
}
return output;
}
/**
* Returns the JSON representation used when this object is serialized.
* @returns The JSON-serializable representation.
* @example
* ```ts
* import { GraphQLError } from 'graphql/error';
*
* const error = new GraphQLError('Resolver failed.', {
* path: ['viewer', 'name'],
* extensions: { code: 'INTERNAL' },
* });
*
* error.toJSON(); // => { message: 'Resolver failed.', path: ['viewer', 'name'], extensions: { code: 'INTERNAL' } }
* ```
*/
toJSON(): GraphQLFormattedError {
type WritableFormattedError = {
-readonly [P in keyof GraphQLFormattedError]: GraphQLFormattedError[P];
};
const formattedError: WritableFormattedError = {
message: this.message,
};
if (this.locations != null) {
formattedError.locations = this.locations;
}
if (this.path != null) {
formattedError.path = this.path;
}
if (this.extensions != null && Object.keys(this.extensions).length > 0) {
formattedError.extensions = this.extensions;
}
return formattedError;
}
}
function undefinedIfEmpty<T>(
array: Array<T> | undefined,
): Array<T> | undefined {
return array === undefined || array.length === 0 ? undefined : array;
}
/** See: https://spec.graphql.org/draft/#sec-Errors */
export interface GraphQLFormattedError {
/**
* A short, human-readable summary of the problem that **SHOULD NOT** change
* from occurrence to occurrence of the problem, except for purposes of
* localization.
*/
readonly message: string;
/**
* If an error can be associated to a particular point in the requested
* GraphQL document, it should contain a list of locations.
*/
readonly locations?: ReadonlyArray<SourceLocation>;
/**
* If an error can be associated to a particular field in the GraphQL result,
* it _must_ contain an entry with the key `path` that details the path of
* the response field which experienced the error. This allows clients to
* identify whether a null result is intentional or caused by a runtime error.
*/
readonly path?: ReadonlyArray<string | number>;
/**
* Reserved for implementors to extend the protocol however they see fit,
* and hence there are no additional restrictions on its contents.
*/
readonly extensions?: GraphQLFormattedErrorExtensions;
}
/**
* Prints a GraphQLError to a string, representing useful location information
* about the error's position in the source. This deprecated helper is retained
* for backwards compatibility; call `error.toString()` instead because
* printError will be removed in v17.
* @param error - The error to format.
* @returns The printed string representation.
* @example
* ```ts
* import { GraphQLError, printError } from 'graphql/error';
*
* const message = printError(new GraphQLError('Example error'));
*
* message; // => 'Example error'
* ```
* @deprecated Please use `error.toString` instead. Will be removed in v17
*/
export function printError(error: GraphQLError): string {
return error.toString();
}
/**
* Given a GraphQLError, format it according to the rules described by the
* Response Format, Errors section of the GraphQL Specification. This deprecated
* helper is retained for backwards compatibility; call `error.toJSON()`
* instead because formatError will be removed in v17.
* @param error - The error to format.
* @returns The JSON-serializable formatted error.
* @example
* ```ts
* import { GraphQLError, formatError } from 'graphql/error';
*
* const formatted = formatError(new GraphQLError('Example error'));
*
* formatted; // => { message: 'Example error' }
* ```
* @deprecated Please use `error.toJSON` instead. Will be removed in v17
*/
export function formatError(error: GraphQLError): GraphQLFormattedError {
return error.toJSON();
}