Graphql коды ошибок

conf-talks

В любом приложении возникают ошибки, и в вашим GraphQL API они тоже будут. Се ля ви.

Как работать с ошибками в GraphQL? К чему необходимо быть готовым клиентам вашего АПИ? Как лучше возвращать ошибки клиенту? Да и как вообще они возвращаются в GraphQL? В этот статье мы разберем как работать с ошибками в GraphQL.

Для начала давайте бегло посмотрим какие ошибки могут возникать и сразу разобьем их на группы:

  • ФАТАЛЬНЫЕ ОШИБКИ
    • 500 Internal Server Error
    • кончилась память
    • забыли установить пакет
    • грубая синтаксическая ошибка в коде
  • ОШИБКИ ВАЛИДАЦИИ
    • ошибка невалидного GraphQL-запроса
    • запросили несуществующее поле
    • не передали обязательный аргумент
    • не передали переменную
  • RUNTIME ОШИБКИ В RESOLVE-МЕТОДАХ
    • throw new Error(“”)
    • undefined is not a function (юзайте Flowtype или TypeScript уже в конце концов)
    • ошибка невалидного значения в return
  • ПОЛЬЗОВАТЕЛЬСКИЕ ОШИБКИ
    • запись не найдена
    • недостаточно прав для просмотра или редактирования записи

Как обычно GraphQL-сервер отвечает на ошибки?

Если произошла фатальная ошибка, то сервер возвращает 500 код. Это как обычно.

Но вот что необычное в GraphQL, так если произошла любая другая ошибка сервер возвращает код 200. Обычно бывалые REST API разработчики на этом моменте хотят выпрыгнуть из окна. Никаких вам 401, 403, 404 и прочих кодов не будет.

Сделали это так, потому что GraphQL по спецификации не привязан ни к какому протоколу. Вы можете гонять GraphQL-запросы через websockets, ssh, telnet ну и обычный http. Коль нет жесткой привязки к протоколу, то ошибки все унесли в тело ответа.

Вот так выглядит ответ от GraphQL по спецификации:

{
  data: {}, // для возврата данных
  errors: [...], // для возврата ошибок, массив между прочим 😳
  extensions: {}, // объект для пользовательских данных, сюда пихайте что хотите
  // другие ключи запрещены по спеке!
}

Первое что бросается в глаза так это то, что GraphQL возвращает массив ошибок. Wow! Т.к. запрос может быть сложный с запросом кучи ресурсов, то GraphQL может вернуть вам часть данных, а на оставшуюся часть вернуть ошибки. И это хорошо, пол ответа лучше, чем ничего.

Фатальные ошибки

Фатальная ошибка чаще всего имеет следующий вид — 500 Internal Server Error. Возникает обычно если кончилась память, забыли установить пакет, совершили грубую синтаксическую ошибку в коде. Да много еще чего. При этом дело не доходит до обработки GraphQL-запроса. И здесь резонно вернуть 500 ошибку.

Нет работы GraphQL, нет кода 200.

Фронтендеры обычно это дело должны обрабатывать на уровне своего Network Layer’a. Получили 500, значит где-то косячнулись бэкендеры с админами.

Ошибки валидации

Сервер получил запрос и делегировал его в пакет graphql. Перед тем как GraphQL-запрос будет выполняться он проходит парсинг и валидацию. Если кривой запрос, то никакие resolve-методы вызваны не будут и тупо будет возвращена ошибка:

{
  errors: [
    {
      message: 'Cannot query field "wrong" on type "Query".',
      locations: [{ line: 3, column: 11 }],
    },
  ],
}

// или например такая
{
  errors: [
    {
      message: 'Variable "$q" of required type "String!" was not provided.',
      locations: [{ line: 2, column: 16 }],
    },
  ],
}

При этом сервер вернет статус 200. При коде 200, ошибка обычно на стороне фронтендера. Но и бекендер может быть к этому причастен, если взял и удалил из схемы какое-то поле. В таком случае все старые работающие приложения теперь стали отправлять невалидные запросы.

Runtime ошибки в resolve-методах

Если запрос прошел парсинг и валидацию, то он начинает выполняться и вызывать resolve-методы вашей схемы согласно присланному GraphQL-запросу. И если вдруг внутри resolve-метода вываливается Exception (throw new Error()), неважно явно вы его выбросили, или он прилетел из недр чужих пакетов. То происходит следующая магия:

  • обработка ветки графа приостанавливается (вложенные resolve-методы вызываться не будут)
  • на месте элемента, где произошла ошибка возвращается null
  • ошибка добавляется в массив errors
  • НО при этом соседние ветки продолжают работать

Хорошо это понять можно на примере следующего кода:

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      search: {
        args: {
          q: { type: GraphQLString },
        },
        resolve: (_, args) => {
          if (!args.q) throw new Error('missing q');
          return { text: args.q };
        },
        type: new GraphQLObjectType({
          name: 'Record',
          fields: {
            text: {
              type: GraphQLString,
              resolve: source => source.text,
            },
          },
        }),
      },
    },
  }),
});

const res = await graphql({
  schema,
  source: `
    query {
      s1: search(q: "ok") { text }
      s2: search { text }
      s3: search(q: "good") { text }
    }
  `,
});

Ответ от сервера будет получен следующий:

{
  errors: [
    { message: 'missing q', locations: [{ line: 4, column: 11 }], path: ['s2'] }
  ],
  data: { s1: { text: 'ok' }, s2: null, s3: { text: 'good' } },
}

Поле s1 возвращает полный результат. В s2 была выброшена ошибка, поэтому оно стало null и в массив errors добавилась ошибка. И дальше поле s3 тоже без проблем вернулось.

Т.е. получается на тех местах, где была выброшена ошибка возвращается null и пишется ошибка в массив. А вся остальная часть запроса продолжает выполняться как ни в чем не бывало. Вот такой вот он добрый GraphQL, хоть что-нибудь да вернет.

Точно также работает, если бэкендер вернул данные неправильного типа в resolve-методе. GraphQL не позволяет вернуть “левые данные” в data.

Вот пример, когда мы по схеме должны вернуть массив строк, но второй элемент не является строкой. Вместо “левого” значения, он вернет null и при этом добавит ошибку в массив:

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      ooops: {
        type: new GraphQLList(GraphQLString),
        resolve: () => ['ok', { hey: 'wrong non String value' }],
      },
    },
  }),
});

const res = await graphql(schema, `query { ooops }`);

expect(res).toEqual({
  errors: [
    {
      message: 'String cannot represent value: { hey: "wrong non String value" }',
      locations: [{ line: 3, column: 11 }],
      path: ['ooops', 1],
    },
  ],
  data: { ooops: ['ok', null] },
});

Также спецификация GraphQL позволяет передать дополнительные данные вместе с ошибкой через проперти extensions. Давайте создадим объект ошибки и присвоим ему два проперти extensions и someOtherData:

new GraphQLObjectType({
  name: 'Query',
  fields: {
    search: {
      resolve: () => {
        const e: any = new Error('Some error');
        e.extensions = { a: 1, b: 2 }; // will be passed in GraphQL-response
        e.someOtherData = { c: 3, d: 4 }; // will be omitted
        throw e;
      },
      type: GraphQLString,
    },
  },
});

На выходе в GraphQL-ответе мы получим следующие данные (extensions будет передан, а все другие проперти из объекта ошибки будут опущены, например не будет someOtherData из нашего примера):

{
  errors: [
    {
      message: 'Some error',
      locations: [{ line: 1, column: 9 }],
      path: ['search'],
      extensions: { a: 1, b: 2 },
    },
  ],
  data: { search: null },
}

Такой механизм позволяет передать клиентам дополнительные данные об ошибке.

Ну коль заговорили про фронтенд, давайте пофантазируем как им работать с такими ошибками. На верхнем уровне одну ошибку в модальном окне вывести не проблема, а если ошибок две? А если у нас сложное приложение и ошибки надо показывать в разных частях приложения? Вот тут у фронтендера начинается просто адская боль и печаль с таким массивом ошибок. Его надо отдельно парсить, понимать какая именно ошибка произошла (например через extensions.code). Как-то передать ошибку в нужную компоненту и на нужный уровень. В общем, приходится сильно изгаляться в коде пробросом лишних проперти и логикой.

Если вам интересно как бэкендер может упростить жизнь фронтендеру, то обязательно читайте следующий раздел.

Пользовательские ошибки

Что такое пользовательские ошибки? Ну это когда вам где-то в приложении надо вывести “запись не найдена”, или “у вас нет прав просматривать этот контент”, или “необходимо подтвердить возраст” или в списке на 23 элементе показать что “запись удалена”.

Если пользоваться стандартным механизмом ошибок GraphQL. То на фронтенде приходится сильно изгаляться, чтобы пробросить ошибку в нужное место.

Но эту проблему можно достаточно элегантно решить, если ошибки возвращать прямо в data на нужном уровне, а не через глобальный массив errors. Для этого в GraphQL есть Union-типы, которые возвращают либо запись с данными, либо ошибку.

Давайте сразу к живому примеру. Представим что нам надо вернуть список видео. Причем какие-то видео в обработке, другие перед просмотром необходимо купить или подтвердить свой возраст. Так давайте и будем возвращать список, который может вернуть Union-тип из Video, VideoInProgressProblem, VideoNeedBuyProblem и VideoApproveAgeProblem. Со стороны фронтендера можно тогда написать вот такой запрос:

query {
  list {
    __typename # <----- магическое поле, которое вернет имя типа для каждой записи
    ...on Video {
      title
      url
    }
    ...on VideoInProgressProblem {
      estimatedTime
    }
    ...on VideoNeedBuyProblem {
      price
    }
    ...on VideoApproveAgeProblem {
      minAge
    }
  }
}

Т.е. используем фрагменты на конкретных типах и запрашиваем поле __typename, которое возвращает имя типа. К запросу выше GraphQL-ответ будет следующий:

{
  data: {
    list: [
      { __typename: 'Video', title: 'DOM2 in the HELL', url: 'https://url' },
      { __typename: 'VideoApproveAgeProblem', minAge: 21 },
      { __typename: 'VideoNeedBuyProblem', price: 10 },
      { __typename: 'VideoInProgressProblem', estimatedTime: 220 },
    ],
  },
}

При таком подходе фронтендер знает какие вообще ошибки могут быть. Также он получает ошибки в нужной компоненте, на нужном уровне. Код захламляется только там, где необходимо разобрать разные варианты пользовательских ошибок и вывести либо данные, либо красивый блок с ошибочкой.

Причем фронтендеры могут легко понять, какой тип ошибки вернулся. И при этом получить дополнительные данные по ошибке, если она их возвращает. Это же просто обычный тип в схеме, который может содержать в себе любые необходимые поля.

Для себя я вынес одно правило, что пользовательским ошибкам лучше всего давать суффикс Problem, а не Error. Это позволяет избежать путаницы как на бэкенде, так и на фронтенде.

Как это дело можно организовать на бэкенде? Достаточно просто. Вот пример:

// Объявляем класс Видео
class Video {
  title: string;
  url: string;

  constructor({ title, url }) {
    this.title = title;
    this.url = url;
  }
}

// И сразу же объявим GraphQL-тип
const VideoType = new GraphQLObjectType({
  name: 'Video',
  fields: () => ({
    title: { type: GraphQLString },
    url: { type: GraphQLString },
  }),
});


// Объявим классы проблем (ошибок)
class VideoInProgressProblem {
  constructor({ estimatedTime }) {
    this.estimatedTime = estimatedTime;
  }
}
class VideoNeedBuyProblem {
  constructor({ price }) {
    this.price = price;
  }
}
class VideoApproveAgeProblem {
  constructor({ minAge }) {
    this.minAge = minAge;
  }
}

// И их типы для GraphQL
const VideoInProgressProblemType = new GraphQLObjectType({
  name: 'VideoInProgressProblem',
  fields: () => ({
    estimatedTime: { type: GraphQLInt },
  }),
});
const VideoNeedBuyProblemType = new GraphQLObjectType({
  name: 'VideoNeedBuyProblem',
  fields: () => ({
    price: { type: GraphQLInt },
  }),
});
const VideoApproveAgeProblemType = new GraphQLObjectType({
  name: 'VideoApproveAgeProblem',
  fields: () => ({
    minAge: { type: GraphQLInt },
  }),
});

// Ну а теперь самое интересное.
// Объявляем наш UNION-тип который будет возвращать либо видео, либо проблему-ошибку
const VideoResultType = new GraphQLUnionType({
  // Даем имя типу.
  // Здорово если если вы выработаете конвенцию в своей команде
  // и к таким Union-типам будете добавлять суффикс Result
  name: 'VideoResult',

  // как хорошие бекендеры добавляем какое-нибудь описание
  description: 'Video or problems',

  // объявляем типы через массив, которые могут быть возвращены
  types: () => [
    VideoType,
    VideoInProgressProblemType,
    VideoNeedBuyProblemType,
    VideoApproveAgeProblemType,
  ],

  // Ну и самое главное надо объявить функцию определения типа.
  // resolve-функции (смотри ниже поле Query.list) просто возвращают JS-объект
  // но вот GraphQL'ю нужно как-то JS-объект, сконвертировать в GraphQL-тип
  // иначе как он узнает что надо записать в поле __typename
  resolveType: value => {
    if (value instanceof Video) {
      return VideoType;
    } else if (value instanceof VideoInProgressProblem) {
      return VideoInProgressProblemType;
    } else if (value instanceof VideoNeedBuyProblem) {
      return VideoNeedBuyProblemType;
    } else if (value instanceof VideoApproveAgeProblem) {
      return VideoApproveAgeProblemType;
    }
    return null;
  },
});

// Ну и вишенка на торте
// Пишем простую схемку, которая нам возвращает массив из Видео и Ошибок-Проблем.
const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: {
      list: {
        type: new GraphQLList(VideoResultType),
        resolve: () => {
          return [
            new Video({ title: 'DOM2 in the HELL', url: 'https://url' }),
            new VideoApproveAgeProblem({ minAge: 21 }),
            new VideoNeedBuyProblem({ price: 10 }),
            new VideoInProgressProblem({ estimatedTime: 220 }),
          ];
        },
      },
    },
  }),
});

Очень просто и красиво. А самое главное удобно для фронтендеров:

  • знают какие ошибки могут быть
  • знают какие поля содержатся в ошибках
  • отлично поддерживается статический анализ, в отличии от обычных ошибок
  • ошибки возвращаются в дереве ответа, а не в глобальном массиве
  • в результате чище, проще и безопаснее код

Любите брата фронтендера своего 😉 Иначе они придут с вилами!

Ссылки по теме

  • Примеры кода в виде тестов к этой статье
  • Видео про ошибки от Sasha Solomon
  • Похожее видео про ошибки от Eloy Durán, всё-таки у Саши лучше

Apollo Server v4 introduced a regression where providing invalid variables yields a 200 status code instead of 400. To mitigate this regression, provide the status400ForVariableCoercionErrors: true option to your ApolloServer constructor. For more information, see the migration guide.

Whenever Apollo Server encounters errors while processing a GraphQL operation, its response to the client includes an errors array containing each error that occurred. Each error in the array has an extensions field that provides additional useful information, including an error code and (while in development mode) a stacktrace.

Here’s an example error response caused by misspelling the __typename field in a query:

To help with debugging, Apollo Server provides an ApolloServerErrorCode enum, which you can use to check if your error is one of the different types produced by Apollo Server.

You can check an error’s code to determine why an error occurred and also add logic to respond to different types of errors, like so:

import { ApolloServerErrorCode } from '@apollo/server/errors';

if (error.extensions?.code === ApolloServerErrorCode.GRAPHQL_PARSE_FAILED) {

} else if (error.extensions?.code === "MY_CUSTOM_CODE") {

Apollo Server’s variety of error codes enables requesting clients to respond differently to different error types. You can also create your own custom errors and codes.

Built-in error codes

Code Description
GRAPHQL_PARSE_FAILED

The GraphQL operation string contains a syntax error.

GRAPHQL_VALIDATION_FAILED

The GraphQL operation is not valid against the server’s schema.

BAD_USER_INPUT

The GraphQL operation includes an invalid value for a field argument.

PERSISTED_QUERY_NOT_FOUND

A client sent the hash of a query string to execute via automatic persisted queries, but the query was not in the APQ cache.

PERSISTED_QUERY_NOT_SUPPORTED

A client sent the hash of a query string to execute via automatic persisted queries, but the server has disabled APQ.

OPERATION_RESOLUTION_FAILURE

The request was parsed successfully and is valid against the server’s schema, but the server couldn’t resolve which operation to run.

This occurs when a request containing multiple named operations doesn’t specify which operation to run (i.e.,operationName), or if the named operation isn’t included in the request.

BAD_REQUEST

An error occurred before your server could attempt to parse the given GraphQL operation.

INTERNAL_SERVER_ERROR

An unspecified error occurred.

When Apollo Server formats an error in a response, it sets the code extension to this value if no other code is set.

Custom errors

You can create a custom errors and codes using the graphql package’s GraphQLError class, like so:

import { GraphQLError } from 'graphql';

throw new GraphQLError(message, {

extensions: { code: 'YOUR_ERROR_CODE', myCustomExtensions },

Custom errors can provide additional context, enabling your clients to understand why an error is happening. We recommend making clear errors for common cases, for example, when a user isn’t logged in (UNAUTHENTICATED), or someone is forbidden from performing an action:

import { GraphQLError } from 'graphql';

throw new GraphQLError('You are not authorized to perform this action.', {

Throwing errors

Apollo Server throws errors automatically when applicable. For example, it throws a GRAPHQL_VALIDATION_FAILED error whenever an incoming operation isn’t valid against the server’s schema.

Your resolvers can also throw errors in situations where Apollo Server doesn’t do so automatically.

For example, this resolver throws a custom error if the integer value provided for a user’s ID is less than 1:

If a resolver throws a generic error that is not a GraphQLError instance, that error is still thrown with an extensions field that includes a stacktrace and code (specifically INTERNAL_SERVER_ERROR), along with any other relevant error details.

Including custom error details

Whenever you throw a GraphQLError, you can add arbitrary fields to the error’s extensions object to provide additional context to the client. You specify these fields in an object you provide to the error’s constructor.

This example builds on the one above by adding the name of the GraphQL argument that was invalid:

This results in a response like the following:

Omitting or including stacktrace

The stacktrace error field is useful while developing and debugging your server, but you probably don’t want to expose it to clients in production.

By default, Apollo Server omits the stacktrace field if the NODE_ENV environment variable is set to either production or test.

You can override this default behavior by passing the includeStacktraceInErrorResponses option to the constructor of ApolloServer. If includeStacktraceInErrorResponses is true, stacktrace is always included. If it’s false, stacktrace is always omitted.

Note that when stacktrace is omitted, it’s also unavailable to your application. To log error stacktraces without including them in responses to clients, see Masking and logging errors.

Masking and logging errors

You can edit Apollo Server error details before they’re passed to a client or reported to Apollo Studio. This enables you to omit sensitive or irrelevant data.

For client responses

In the examples below, we use top-level await calls to start our server asynchronously. Check out our Getting Started guide to see how we configured our project to support this.

The ApolloServer constructor accepts a formatError hook that is run on each error before it’s passed back to the client. You can use this function to log or mask particular errors.

The formatError hook receives two arguments: the first is the error formatted as a JSON object (to be sent with the response), and the second is the original error (wrapped in GraphQLError if thrown by a resolver).

The formatError function does not modify errors that are sent to Apollo Studio as part of usage reporting. See For Apollo Studio reporting.

The below example returns a user-friendly message whenever Apollo Server throws a GRAPHQL_VALIDATION_FAILED error:

import { ApolloServer } from '@apollo/server';

import { startStandaloneServer } from '@apollo/server/standalone';

import { ApolloServerErrorCode } from '@apollo/server/errors';

const server = new ApolloServer({

formatError: (formattedError, error) => {

formattedError.extensions.code ===

ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED

message: "Your query doesn't match the schema. Try double-checking it!",

const { url } = await startStandaloneServer(server);

console.log(`🚀 Server listening at: ${url}`);

As another example, here we return a more generic error whenever the original error’s message begins with Database Error: :

formatError: (formattedError, error) => {

if (formattedError.message.startsWith('Database Error: ')) {

return { message: 'Internal server error' };

If you want to access the originally thrown error (without the JSON formatting), you can use formatError‘s second argument.

For example, if you are using a database package in your app and you’d like to do something when your server throws a specific type of database error:

formatError: (formattedError, error) => {

if (error instanceof CustomDBError) {

Note, if a resolver throws the error, a GraphQLError is wrapped around the initially thrown error. This GraphQLError neatly formats the error and contains useful fields, such as the path where the error occurred.

If you want to remove the outer GraphQLError to access the originally thrown error you can use unwrapResolverError from @apollo/server/errors. The unwrapResolverError function can remove the GraphQLError wrapping from a resolver error or return the error unaltered if it isn’t from a resolver.

So, we can rewrite the above code snippet to work for errors thrown in and outside of resolvers, like so:

import { unwrapResolverError } from '@apollo/server/errors';

formatError: (formattedError, error) => {

if (unwrapResolverError(error) instanceof CustomDBError) {

return { message: 'Internal server error' };

To make context-specific adjustments to the error received by formatError (such as localization or personalization), consider creating a plugin that uses the didEncounterErrors lifecycle event to attach additional properties to the error. These properties can be accessed from formatError.

For Apollo Studio reporting

New in Apollo Server 4: error details are not included in traces by default. Instead, <masked> replaces each error’s message, and the maskedBy error extension replaces all other extensions. The maskedBy extension includes the name of the plugin that performed the masking (ApolloServerPluginUsageReporting or ApolloServerPluginInlineTrace).

You can use Apollo Studio to analyze your server’s error rates. By default, the operations sent to Studio as detailed traces don’t contain error details.

If you do want error information sent to Studio, you can send every error, or you can modify or redact specific errors before they’re transmitted.

To send all errors to Studio you can pass { unmodified: true } to sendErrors, like so:

ApolloServerPluginUsageReporting({

sendErrors: { unmodified: true },

If you want to report specific errors or modify an error before reporting it, you can pass a function to the sendErrors.transform option, like so:

ApolloServerPluginUsageReporting({

if (err.extensions.code === 'MY_CUSTOM_CODE') {

The usage reporting plugin is installed automatically with its default configuration if you provide an Apollo API key to Apollo Server. To customize the usage reporting plugin’s behavior, you need to install it explicitly with a custom configuration, as shown in the examples below.

The function you pass to transform is called for each error (GraphQLError) to be reported to Studio. The error is provided as the function’s first argument. The function can either:

  • Return a modified form of the error (e.g., by changing the err.message to remove potentially sensitive information)
  • Return null to prevent the error from being reported entirely

Note that returning null also affects Studio’s aggregated statistics about how many operations contain errors and at what paths those errors appear.

As mentioned above, you can use the unwrapResolverError (from @apollo/server/errors) to remove the GraphQLError wrapping an original error.

For federated graphs, define your transform function in each subgraph’s inline trace plugin to rewrite field errors. If you want to transform your gateway’s parsing or validation errors, you can define your transform function in your gateway.

Example: Ignoring common low-severity errors

Let’s say our server is throwing an UNAUTHENTICATED error whenever a user enters an incorrect password. We can avoid reporting these errors to Apollo Studio by defining a transform function, like so:

import { ApolloServer } from '@apollo/server';

import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';

const server = new ApolloServer({

ApolloServerPluginUsageReporting({

if (err.extensions.code === 'UNAUTHENTICATED') {

This example configuration ensures that any UNAUTHENTICATED error that’s thrown within a resolver is only reported to the client, and never sent to Apollo Studio. All other errors are transmitted to Studio normally.

Example: Filtering errors based on other properties

When generating an error (e.g., new GraphQLError("Failure!")), the error’s message is the most common extension (in this case it’s Failure!). However, any number of extensions can be attached to the error (such as a code extension).

We can check these extensions when determining whether an error should be reported to Apollo Studio using the transform function as follows:

import { ApolloServer } from '@apollo/server';

import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';

const server = new ApolloServer({

ApolloServerPluginUsageReporting({

if (err.message && err.message.startsWith('Known error message')) {

This example configuration ensures that any error that starts with Known error message is not transmitted to Apollo Studio, but all other errors are sent as normal.

Example: Redacting information from an error message

As mentioned above, by default, the operations sent to Studio as detailed traces don’t contain error details.

If you do want to send an error’s details to Apollo Studio, but need to redact some information first, the transform function can help.

For example, if there is personally identifiable information in the error message, like an API key:

import { GraphQLError } from 'graphql';

"The x-api-key:12345 doesn't have sufficient privileges.",

The transform function can ensure that such information is not sent to Apollo Studio and potentially revealed outside its intended scope:

import { ApolloServer } from '@apollo/server';

import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';

const server = new ApolloServer({

ApolloServerPluginUsageReporting({

err.message = err.message.replace(/x-api-key:[A-Z0-9-]+/, 'REDACTED');

In this case, the error above is reported to Apollo Studio as:

The REDACTED doesn't have sufficient privileges.

Setting HTTP status code and headers

GraphQL, by design, does not use the same conventions from REST to communicate via HTTP verbs and status codes. Client information should be contained in the schema or as part of the standard response errors field. We recommend using the included Error Codes or Custom Errors for error consistency rather than directly modifying the HTTP response.

Apollo Server uses different HTTP status codes in various situations:

  • If Apollo Server hasn’t correctly started up or is in the process of shutting down, it responds with a 500 status code.
  • The former can happen if you use a serverless integration and it sends requests to an Apollo Server instance that had an error on startup. The latter happens if you aren’t properly draining your server.
  • If Apollo Server can’t parse the request into a legal GraphQL document and validate it against your schema, it responds with a 400 status code. This can also happen with other request problems, such as if a client attempts to send a batched HTTP request when allowBatchedHttpRequests isn’t enabled or if CSRF prevention blocks a request.
  • If a request uses an invalid HTTP method (GET with a mutation, or any HTTP method other than GET or POST), then Apollo Server responds with a 405 status code.
  • If your context function throws, Apollo Server responds with a 500 status code.
  • If there is an unexpected error during the processing of the request (either a bug in Apollo Server or a plugin hook throws), Apollo Server responds with a 500 status code.
  • Otherwise, Apollo Server returns a 200 status code. This is essentially the case where the server can execute the GraphQL operation, and execution completes successfully (though this can still include resolver-specific errors).

There are three ways to change an HTTP status code or set custom response headers, you can: throw an error in a resolver, throw an error in your context function, or write a plugin.

While Apollo Server does enable you to set HTTP status codes based on errors thrown by resolvers, best practices for GraphQL over HTTP encourage sending 200 whenever an operation executes. So, we don’t recommend using this mechanism in resolvers, just in the context function or in a plugin hooking into an early stage of the request pipeline.

Be aware that GraphQL client libraries might not treat all response status codes the same, so it will be up to your team to decide which patterns to use.

To change the HTTP status code and response headers based on an error thrown in either a resolver or context function, throw a GraphQLError with an http extension, like so:

import { GraphQLError } from 'graphql';

throw new GraphQLError('the error message', {

code: 'SOMETHING_BAD_HAPPENED',

['some-header', 'it was bad'],

['another-header', 'seriously'],

import { GraphQLError } from 'graphql';

throw new GraphQLError('the error message', {

code: 'SOMETHING_BAD_HAPPENED',

['some-header', 'it was bad'],

['another-header', 'seriously'],

You don’t need to include status unless you want to override the default status code (200 for a resolver or 500 for a context function). The optional headers field should provide a Map with lowercase header names.

If your setup includes multiple resolvers which throw errors that set status codes or set the same header, Apollo Server might resolve this conflict in an arbitrary way (which could change in future versions). Instead, we recommend writing a plugin (as shown below).

You can also set the HTTP status code and headers from a plugin. As an example, here is how you could set a custom response header and status code based on a GraphQL error:

async requestDidStart() {

async willSendResponse({ response }) {

response.http.headers.set('custom-header', 'hello');

if (response.body.kind === 'single' &&

response.body.singleResult.errors?.[0]?.extensions?.code === 'TEAPOT') {

response.http.status = 418;

const server = new ApolloServer({

plugins: [setHttpPlugin],

There are a lot of different kinds of errors in GraphQL! In this guide, we’ll discuss some of the main categories and learn when they apply.

Validation Errors

Because GraphQL is strongly typed, it performs validation of all queries before executing them. If an incoming query is invalid, it isn’t executed. Instead, a response is sent back with "errors":

Each error has a message, line, column and path.

The validation rules are part of the GraphQL specification and built into GraphQL-Ruby, so there’s not really a way to customize this behavior, except to pass validate: false when executing a query, which skips validation altogether.

You can configure your schema to stop validating after a certain number of errors by setting Schema.validate_max_errors. Also, you can add a timeout to this step with Schema.validate_timeout.

Analysis Errors

GraphQL-Ruby supports pre-execution analysis, which may return "errors" instead of running a query. You can find details in the Analysis guide.

GraphQL Invariants

While GraphQL-Ruby is executing a query, some constraints must be satisfied. For example:

  • Non-null fields may not return nil.
  • Interface and union types must resolve objects to types that belong to that interface/union.

These constraints are part of the GraphQL specification, and when they are violated, it must be addressed somehow. Read more in Type Errors.

Top-level "errors"

The GraphQL specification provides for a top-level "errors" key which may include information about errors during query execution. "errors" and "data" may both be present in the case of a partial success.

In your own schema, you can add to the "errors" key by raising GraphQL::ExecutionError (or subclasses of it) in your code. Read more in the Execution Errors guide.

Handled Errors

A schema can be configured to handle certain errors during field execution with handlers that you give it, using rescue_from. Read more in the Error Handling guide.

Unhandled Errors (Crashes)

When a raised error is not rescued, the GraphQL query crashes entirely and the surrounding code (like a Rails controller) must handle the exception.

For example, Rails will probably return a generic 500 page.

Errors as Data

When you want end users (human beings) to read error messages, you can express errors in the schema, using normal GraphQL fields and types. In this approach, errors are strongly-typed data, queryable in the schema, like any other application data.

For more about this approach, see Mutation Errors

Response

When a GraphQL service receives a request, it must return a well-formed
response. The service’s response describes the result of executing the requested
operation if successful, and describes any errors raised during the request.

A response may contain both a partial response as well as a list of errors in
the case that any field error was raised on a field and was replaced with
{null}.

Response Format

A response to a GraphQL request must be a map.

If the request raised any errors, the response map must contain an entry with
key errors. The value of this entry is described in the «Errors» section. If
the request completed without raising any errors, this entry must not be
present.

If the request included execution, the response map must contain an entry with
key data. The value of this entry is described in the «Data» section. If the
request failed before execution, due to a syntax error, missing information, or
validation error, this entry must not be present.

The response map may also contain an entry with key extensions. This entry, if
set, must have a map as its value. This entry is reserved for implementors to
extend the protocol however they see fit, and hence there are no additional
restrictions on its contents.

To ensure future changes to the protocol do not break existing services and
clients, the top level response map must not contain any entries other than the
three described above.

Note: When errors is present in the response, it may be helpful for it to
appear first when serialized to make it more clear when errors are present in a
response during debugging.

Data

The data entry in the response will be the result of the execution of the
requested operation. If the operation was a query, this output will be an object
of the query root operation type; if the operation was a mutation, this output
will be an object of the mutation root operation type.

If an error was raised before execution begins, the data entry should not be
present in the result.

If an error was raised during the execution that prevented a valid response, the
data entry in the response should be null.

Errors

The errors entry in the response is a non-empty list of errors raised during
the request, where each error is a map of data described by the error result
format below.

If present, the errors entry in the response must contain at least one error.
If no errors were raised during the request, the errors entry must not be
present in the result.

If the data entry in the response is not present, the errors entry must be
present. It must contain at least one request error indicating why no data was
able to be returned.

If the data entry in the response is present (including if it is the value
{null}), the errors entry must be present if and only if one or more field
error
was raised during execution.

Request Errors

:: A request error is an error raised during a request which results in no
response data. Typically raised before execution begins, a request error may
occur due to a parse grammar or validation error in the Document, an inability
to determine which operation to execute, or invalid input values for variables.

A request error is typically the fault of the requesting client.

If a request error is raised, the data entry in the response must not be
present, the errors entry must include the error, and request execution should
be halted.

Field Errors

:: A field error is an error raised during the execution of a particular field
which results in partial response data. This may occur due to an internal error
during value resolution or failure to coerce the resulting value.

A field error is typically the fault of a GraphQL service.

If a field error is raised, execution attempts to continue and a partial result
is produced (see Handling Field Errors). The
data entry in the response must be present. The errors entry should include
this error.

Error Result Format

Every error must contain an entry with the key message with a string
description of the error intended for the developer as a guide to understand and
correct the error.

If an error can be associated to a particular point in the requested GraphQL
document, it should contain an entry with the key locations with a list of
locations, where each location is a map with the keys line and column, both
positive numbers starting from 1 which describe the beginning of an associated
syntax element.

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.

This field should be a list of path segments starting at the root of the
response and ending with the field associated with the error. Path segments that
represent fields should be strings, and path segments that represent list
indices should be 0-indexed integers. If the error happens in an aliased field,
the path to the error should use the aliased name, since it represents a path in
the response, not in the request.

For example, if fetching one of the friends’ names fails in the following
operation:

{
  hero(episode: $episode) {
    name
    heroFriends: friends {
      id
      name
    }
  }
}

The response might look like:

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [{ "line": 6, "column": 7 }],
      "path": ["hero", "heroFriends", 1, "name"]
    }
  ],
  "data": {
    "hero": {
      "name": "R2-D2",
      "heroFriends": [
        {
          "id": "1000",
          "name": "Luke Skywalker"
        },
        {
          "id": "1002",
          "name": null
        },
        {
          "id": "1003",
          "name": "Leia Organa"
        }
      ]
    }
  }
}

If the field which experienced an error was declared as Non-Null, the null
result will bubble up to the next nullable field. In that case, the path for
the error should include the full path to the result field where the error was
raised, even if that field is not present in the response.

For example, if the name field from above had declared a Non-Null return
type in the schema, the result would look different but the error reported would
be the same:

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [{ "line": 6, "column": 7 }],
      "path": ["hero", "heroFriends", 1, "name"]
    }
  ],
  "data": {
    "hero": {
      "name": "R2-D2",
      "heroFriends": [
        {
          "id": "1000",
          "name": "Luke Skywalker"
        },
        null,
        {
          "id": "1003",
          "name": "Leia Organa"
        }
      ]
    }
  }
}

GraphQL services may provide an additional entry to errors with key
extensions. This entry, if set, must have a map as its value. This entry is
reserved for implementors to add additional information to errors however they
see fit, and there are no additional restrictions on its contents.

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [{ "line": 6, "column": 7 }],
      "path": ["hero", "heroFriends", 1, "name"],
      "extensions": {
        "code": "CAN_NOT_FETCH_BY_ID",
        "timestamp": "Fri Feb 9 14:33:09 UTC 2018"
      }
    }
  ]
}

GraphQL services should not provide any additional entries to the error format
since they could conflict with additional entries that may be added in future
versions of this specification.

Note: Previous versions of this spec did not describe the extensions entry for
error formatting. While non-specified entries are not violations, they are still
discouraged.

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [{ "line": 6, "column": 7 }],
      "path": ["hero", "heroFriends", 1, "name"],
      "code": "CAN_NOT_FETCH_BY_ID",
      "timestamp": "Fri Feb 9 14:33:09 UTC 2018"
    }
  ]
}

Serialization Format

GraphQL does not require a specific serialization format. However, clients
should use a serialization format that supports the major primitives in the
GraphQL response. In particular, the serialization format must at least support
representations of the following four primitives:

  • Map
  • List
  • String
  • Null

A serialization format should also support the following primitives, each
representing one of the common GraphQL scalar types, however a string or simpler
primitive may be used as a substitute if any are not directly supported:

  • Boolean
  • Int
  • Float
  • Enum Value

This is not meant to be an exhaustive list of what a serialization format may
encode. For example custom scalars representing a Date, Time, URI, or number
with a different precision may be represented in whichever relevant format a
given serialization format may support.

JSON Serialization

JSON is the most common serialization format for GraphQL. Though as mentioned
above, GraphQL does not require a specific serialization format.

When using JSON as a serialization of GraphQL responses, the following JSON
values should be used to encode the related GraphQL values:

GraphQL Value JSON Value
Map Object
List Array
Null {null}
String String
Boolean {true} or {false}
Int Number
Float Number
Enum Value String

Note: For consistency and ease of notation, examples of responses are given in
JSON format throughout this document.

Serialized Map Ordering

Since the result of evaluating a selection set is ordered, the serialized Map of
results should preserve this order by writing the map entries in the same order
as those fields were requested as defined by selection set execution. Producing
a serialized response where fields are represented in the same order in which
they appear in the request improves human readability during debugging and
enables more efficient parsing of responses if the order of properties can be
anticipated.

Serialization formats which represent an ordered map should preserve the order
of requested fields as defined by {CollectFields()} in the Execution section.
Serialization formats which only represent unordered maps but where order is
still implicit in the serialization’s textual order (such as JSON) should
preserve the order of requested fields textually.

For example, if the request was { name, age }, a GraphQL service responding in
JSON should respond with { "name": "Mark", "age": 30 } and should not respond
with { "age": 30, "name": "Mark" }.

While JSON Objects are specified as an
unordered collection of key-value pairs
the pairs are represented in an ordered manner. In other words, while the JSON
strings { "name": "Mark", "age": 30 } and { "age": 30, "name": "Mark" }
encode the same value, they also have observably different property orderings.

Note: This does not violate the JSON spec, as clients may still interpret
objects in the response as unordered Maps and arrive at a valid value.

The GraphQL spec defines a set of validations for the GraphQL server to perform when resolving the query. If some validation fails, the corresponding error message must be returned under the errors entry of the GraphQL response. The actual message to return is not defined in the spec, hence it is customized by each server. More importantly, the spec does not define a code to uniquely identify each of the errors.

In other words, there is no concept of an “error code” in the GraphQL spec — only validations to perform. Indeed, there’s no code entry for the error in the response, only message (and also locations, to indicate where the error is produced in the query). In order to return an error code, the GraphQL server must add it under the extensions sub-entry of the error, which GraphQL servers can use for whatever custom feature they support, and use an error code of its own conception.

For the actual error code to return, servers may currently use one of these alternatives:

  1. The section of the GraphQL spec under which the corresponding validation is defined
  2. Use a custom error code — i.e., one not defined at the spec level

For instance, whenever a mandatory argument is missing in a given field in the GraphQL query, the GraphQL server may return error code 5.4.2.1 in the first case, or "gql0001" (or something else) in the second case.

Some other languages/frameworks do make use of error codes, such as Rust, React, and TypeScript. Inspired by these other language and framework experiences, a couple of issues requesting to add generic error codes to the GraphQL spec were proposed two years ago:

  1. #698 – Discussion: Generic error codes in the spec
  2. #708 – [RFC] Standard Error Codes

Both issues had received immediate endorsement from the community, but no PR has come out out of them, and neither of them has seen any activity in the last year.

As the maintainer of my own GraphQL server, I often browse the list of proposals for the GraphQL spec, attempting to identify my next task to implement. I explored the issues concerning generic error codes and found them compelling, so I decided to support them ahead of a PR and not wait for them to become merged to the spec (which also may never happen).

In this article, I’ll share a few insights I uncovered from implementing this feature on my GraphQL server, including:

  • The potential benefits of generic error codes in GraphQL
  • Designing and implementing the solution
  • Insights from the implementation

In the conclusion, I’ll give my opinion if supporting generic error codes in the GraphQL spec may be worth pursuing or not.

The potential benefits of generic error codes in GraphQL

Currently, all GraphQL servers must implement their own test suites to make sure that the validations are supported and working as expected. As every server will return a custom error message for the failing validation — for instance, some server may return Mandatory argument '{0}' in field '{1}' is missing, while another one may return You must provide a value for argument '{0}' — their tests cannot be reused across servers:

{
  "errors": [
    {
      "message": "Mandatory argument 'id' in field 'user' is missing",
      "locations": [ { "line": 1, "column": 12 } ]
    }
  ]
}

But returning a generic error code alongside the error message could solve this. The test suite could be executed against the error code, which would be known to all servers because it would be defined at the spec level.

In this scenario, whenever there is an error, the server will return a code entry, in addition to the message and locations entries:

{
  "errors": [
    {
      "message": "Mandatory argument 'id' in field 'user' is missing",
      "locations": [ { "line": 1, "column": 12 } ],
      "code": "1"
    }
  ]
}

This is indeed the goal stated by the first of the issues, which advances the idea of supporting a generic test suite that could work for all GraphQL servers:

I am researching and planning to build a generic test suite to check the compliance of the various GraphQL libraries […]. There are various implementations of GraphQL and all of them are throwing errors in various ways.

As all errors are thrown in plain text(string). There is no way to test these implementations for such errors.

Say we have given an incorrect query (with circular fragment) to a GraphQL server and we are expecting an error. If the error comes with a specific error code then it would be great to test the libraries and provides and assuring the actual cause of the error, therefore, improving the DX as well.

Using generic error codes could also simplify the logic in our applications whenever it needs to make sense of the error, and make it easier to swap the GraphQL server provider. For instance, if the application needs to show different layouts depending on the severity of the error, this logic could react based on the error code, and if the code is generic, the same logic will work after migrating to a different GraphQL server.

By supporting generic error codes, we can possibly centralize the definitions for all error messages and codes in the application, even defining them all in a single file, which can help developers understand all possible contingencies that must be taken care of. This is the case with React, which defines all its error codes in a single JSON file.

Finally, the same solution also provides the opportunity to return even more information. In particular, a specifiedBy entry could point to the online documentation that explains why the error happens and how to solve it:

{
  "errors": [
    {
      "message": "Mandatory argument 'id' in field 'user' is missing",
      "locations": [ { "line": 1, "column": 12 } ],
      "code": "1",
      "specifiedBy": "https://spec.graphql.org/October2021/#sec-Required-Arguments"
    }
  ]
}

This is the case with Rust, whose online documentation provides a detailed guide for developers to troubleshoot and understand the language.

Designing and implementing the solution

I have recently implemented the above feature in my GraphQL server so that it also returns code and specifiedBy entries for each error.

As I mentioned before, these are still custom features and are not supported by the GraphQL spec and they must appear under the extensions sub-entry. Only once the proposal is approved and merged into the spec — if it ever is — can those new entries appear at the root of the error entry.

Currently, the error codes are not generic, since that depends on the GraphQL spec defining them. Instead, I have decided to use the validation section number for my specific error code.

Now, when executing the following faulty query (because variable $limit is not defined in the operation):

{
  posts(pagination: { limit: $limit }) {
    id
    title
  }
}

…the server attaches the code and specifiedBy elements to the error, in addition to the expected message and locations entries:

{
  "errors": [
    {
      "message": "...",
      "locations": [...],
      "extensions": {
        "code": "gql-5.8.3",
        "specifiedBy": "https://spec.graphal.org/draft/#sec-All-Variable-Uses-Defined"
      }
    }
  ]
}

The GraphQL response contains error codes

Below, I detail some design and implementation decisions I made and why I made them, as well as how they affect the usefulness of generic error codes in GraphQL.

Dealing with different types of errors

When we are creating a GraphQL API, there are different kinds of errors that the API may return:

  1. GraphQL spec errors: those relevant to parsing, validating and executing the GraphQL document, covered by the spec under the Language, Validation and Execution sections, respectively
  2. Client errors: those under the domain of the application, such as validating that inputs have a certain length or format
  3. Server errors: those produced when a runtime operation fails, such as resolving a field that fetches data from an external data source and the connection doesn’t go through

Client and server errors are not simply “all the other” errors: they are quite different in nature. Client errors are known in advance, based on the rules defined for the application, and they are public, so the corresponding error message must be added to the response to inform the user of the problem.

Server errors, by contrast, mostly arise from unexpected events, such as a server or database going down. They are private because they could expose security-sensitive data (such as an API key or database connection credentials), so the user must receive a vague There was an unexpected error message, while the detailed error message must only be provided to the admin of the application.

When we are discussing generic error codes for GraphQL, we are originally dealing with the first type of errors only, GraphQL spec errors. But this doesn’t need to be the case — the GraphQL server has the opportunity to generalize the same concept, and its associated logic, for all three types of errors.

This generalization can produce several additional benefits:

  • The developer will only have to learn a single way to define errors for the API
  • The user will always receive the error in the same format
  • Error codes can be managed in a modular way, as I’ll explain below

The modular approach to managing error codes

Defining all error codes for the complete API in a single file (as I mentioned earlier) would no longer be possible in a modular approach because the different kinds of errors will necessarily be implemented at different layers:

  • The GraphQL spec and server errors will be implemented at the GraphQL spec layer
  • Server and client errors will be implemented at the application layer

At most, only the GraphQL spec errors can be defined in a single file (as done here), and server and client errors could be defined all together for the API, or possibly in different files using a modular approach (as I’ve demonstrated here and here), and in every module installed for the API, which can return errors.

If we needed to have an overview of all errors in the API, the GraphQL server could support retrieving them via introspection, instead of finding and storing them all in a single place.

Selecting the error codes and appropriate online docs

Because of the different types of errors, the GraphQL server could use different strategies to select what error code to return, and what online documentation to point to:

  1. For GraphQL spec errors:
    1. Use generic error codes defined in the spec (once provided)
    2. specifiedBy could point to the corresponding section in spec.graphql.org/draft
  2. For server errors:
    1. Use custom error codes defined by the GraphQL server
    2. specifiedBy could point to documentation in the GraphQL server vendor’s website
  3. For client errors:
    1. Use custom error codes defined by the API developer
    2. specifiedBy could point to the public API’s technical docs (or, possibly, a section alongside the public schema docs) if it is needed, such as if the error message is not descriptive enough, as an input validation should already be

Format of the error code

As is perhaps obvious at this point, different types of errors will return different error codes, and these can use a different format.

For GraphQL spec errors, the format could directly use the spec section code, such as "gql-{section-code}" (producing code "gql-5.4.2.1"), which allows us to quickly understand which error in the GraphQL spec is being referenced. The spec section code is the natural choice in order to establish the same error code across different GraphQL servers, until the corresponding generic error codes are defined in the spec.

However, this solution is not ideal because one section could require more than one validation to perform; hence, it will not map one-to-one with an error code/message. For instance, section 6.1 Executing Requests may return one of four different error messages, for which an additional suffix a-d was also added to their codes on my server:

  • gql-6.1.a: Operation with name '%s' does not exist
  • gql-6.1.b: When the document contains more than one operation, the operation name to execute must be provided
  • gql-6.1.c: The query has not been provided
  • gql-6.1.d: No operations defined in the document

For client and server errors, which are custom errors), we could use the format "{namespace}{feedbackType}{counter}", where:

  • The {namespace} ("PoPComponentModel") is unique per module installed on the API (more on this later on)
  • {feedbackType} ("e" for “error”) represents the type of “feedback”, where an “error” is just one possible option among several (more on this later on)
  • {counter} ("24") is just an increasing number to create a unique code per message within each module

Our custom GraphQL error code appears in the response

Following this format would produce code "PoPComponentModele24" in my GraphQL server.

Namespacing error codes

An error code is an arbitrary string that has a single requirement: it must be unique, identifying a single error message. Otherwise, if the same error code were assigned to two different error messages, then it wouldn’t be very useful.

As stated earlier on, different modules in the application could return their own errors. Since different errors happen at different layers of the server and application, and different modules may be provided by different providers (such as using some module offered by one third party, another one created by the API development team, and so on), creating a system that assigns a unique code per error quickly becomes difficult.

For this reason, it makes sense simply to “namespace” error codes when printing them in the GraphQL response. This way, two different modules can both internally use error code "1", and these will be treated as "module11" and "module21" by the application. As long as each namespace is unique per module, then all produced error codes will also be unique.

Providing other types of feedback messages

If we pay attention to the error messages in TypeScript, we will notice that, in addition to code, they also have a category. Errors will have a category with value "Error", but there are also entries with other values, namely "Message" and "Suggestion":

{
  "'use strict' directive used here.": {
    "category": "Error",
    "code": 1349
  },
  "Print the final configuration instead of building.": {
    "category": "Message",
    "code": 1350
  },
  "File is a CommonJS module; it may be converted to an ES module.": {
    "category": "Suggestion",
    "code": 80001
  }
}

In TypeScript, “errors” are simply one type of feedback that the application can provide to the users. There is no reason why GraphQL could not implement the same idea.

Implementing error codes provides an opportunity for GraphQL servers to also support this capability. Since this behavior is not documented in the GraphQL spec, these additional feedback entries will need to be returned under the root extensions entry in the response.

It is to distinguish among these different feedback types that earlier on I suggested we add {feedbackType} to the entry code, with value "e" for “error”, "w" for “warning”, and "s" for “suggestion”, to start.

The GraphQL spec currently covers two types of feedback: “errors” and “deprecations”. These two can internally be treated as “feedback” by the GraphQL server, so that the same logic can handle both of them (or any other type of feedback message). Then, the specifiedBy entry could conveniently also be assigned to deprecations — to further explain why some field in the schema was deprecated, describe related deprecations expected for the API in the future, and others.

For my GraphQL server, I have decided to support these categories, in addition to errors and deprecations:

  • Warning
  • Notice
  • Suggestion
  • Log

Messages from these categories provide extra nuance, since not everything is an error. For instance, a query using @export to assign two different values to the same variable does not necessarily need to be a halting issue (returning null in the corresponding field), and I’d rather return a warning instead:

GraphQL response containing a warning

Supporting different types of feedback messages does not complicate the task for the API developers, since defining errors, warnings, or suggestions is based on the same underlying code.

Insights from the implementation

What benefits and disadvantages would be produced by imposing GraphQL servers to return error codes in addition to messages, and making those codes generic by defining them in the GraphQL spec?

The following are my impressions, based on my experience adding support for this feature on my server.

Still no generic error codes, but that’s easy to correct

As there are no generic error codes yet, I’ve used the corresponding section on the spec as the error code. But this is easy to correct: should the issue be approved and merged into the spec, I must only update the codes from PHP constants defined in a single file, and in all references to them spread across the server code, which I can easily find using an editor (in my case, VSCode).

Then, GraphQL servers can decide to implement this feature, and upgrade it in the future at a negligible cost.

No breaking changes concerning the GraphQL spec

Returning error codes is not a breaking change, because the new feature adds a new entry code in the response, without modifying anything else.

For the time being, this entry must be placed under extensions. If it ever becomes merged into the spec, then code can be moved one level up, which is a minimal change.

Breaking changes concerning the server code

Currently, as the GraphQL response must only include the error message, GraphQL servers need only pass a single string to denote the error, as in this code by graphql-js:

throw new GraphQLError(
  'Schema does not define the required query root type.',
  operation,
);

If error codes were to be returned, the GraphQL spec errors should be mandatory (or they’d provide no value), while the server/client errors could be made optional, allowing developers to decide if to support error codes for their own APIs or not (since these error codes are custom to the application, they don’t serve the purpose of standardizing a response across different servers, making them less useful).

As such, all instances where an error is produced in the GraphQL server’s codebase must be adapted to also provide the error code. The APIs based on them could be adapted too, but they wouldn’t necessarily have to.

In addition, it would make sense to transfer all error messages within each module to a single file, as to ensure that a unique error code is assigned to each error message. However, this task could require a considerable refactoring of the GraphQL server code, and possibly of the implementing APIs too (i.e., if the underlying architecture changes, APIs may be forced to adapt their code too).

In summary, it could make sense for GraphQL servers to support error codes, but possibly only if they don’t need to change their current architecture drastically, as to not require implementing APIs to be adapted too (or risk losing users).

On a different note, all tests already implemented by the GraphQL server could be adapted to be based on the error codes, but that’s most likely an optional task: if the error message-based tests are working nowadays, testing against them should still work after introducing the additional error code on the response.

Unit tests can have a better foundation

When testing the GraphQL server, we may want to validate against the error code, which succinctly conveys what the problem is instead of the error message, which also gives unneeded information, and which may occasionally be updated (whereas the code will remain the same).

In addition, if placing all errors in a single place per module, we can also obtain the error message just by invoking some function (which must receive the error code, and possibly some params to customize the message). This way, if the error message gets updated, the unit tests will still work without the need to update the error message also in them.

For instance, validating 5.8.3 All Variable Uses Defined for my server is done like this:

public function testVariableMissing(): void
{
  $this->expectException(InvalidRequestException::class);
  $this->expectExceptionMessage(
    (
      new FeedbackItemResolution(
        GraphQLSpecErrorFeedbackItemProvider::class, GraphQLSpecErrorFeedbackItemProvider::E_5_8_3,
        ['missingVar']
      )
    )->getMessage()
  );
  $this->getParser()->parse('
    query SomeOperation {
      foo(bar: $missingVar) {
        id
      }
    }
  ')->validate();
}

Track unimplemented validations

Using a central place to manage all errors makes it easier to track those validations which have not been implemented yet.

For instance, my server has all GraphQL spec validations defined in a single file, but those which are not satisfied yet have been disabled and given the description "TODO: satisfy", making it easy for me to identify them.

Better overall picture of all errors, but more difficult to visualize the actual validation code

Moving all error codes to a central location makes it easier to have a complete understanding of the application, which is a good thing, but at the same time it makes it more difficult to understand the actual pieces of code executing the validation, at least if the error code is not self-explanatory.

For instance, for the client and server errors, my server uses a simple counter for error codes, i.e. "1", "2", and so on, instead of more meaningful codes, such as "field_arg_missing", "field_arg_cant_be_negative" and so on. That’s because I was lazy, and because naming error codes is hard.

To some extent, the same situation happens with GraphQL spec errors too. Error codes 5.2.1.1, 5.8.3, and so on, are not as descriptive as error codes "recursive_fragments", "missing_variable", and so on.

As a consequence, when I visualize the code that executes the validation and throws the error if the validation fails, it is not so easy to understand what the error is. The code looks like this:

public function getValue(): mixed
{
  if ($this->variable === null) {
    throw new InvalidRequestException(
      new FeedbackItemResolution(
        GraphQLSpecErrorFeedbackItemProvider::class,
        GraphQLSpecErrorFeedbackItemProvider::E_5_8_3,
        [
          $this->name,
        ]
      )
    );
  }

  return $this->variable->getValue();
}

Before migrating the above piece of code to use error codes, the logic was more understandable:

public function getValue(): mixed
{
  if ($this->variable === null) {
    throw new InvalidRequestException(
      sprintf(
        'Variable '%s' has not been defined in the operation',
        $this->name
      )
    );
  }

  return $this->variable->getValue();
}

Generic test suites? Not so sure about them

The original issue proposed generic error codes as a means to support generic test suites, which would work across different GraphQL servers.

I’m not so sure this would really work out. At least, it would not for my server.

The proposed test suite would need to be platform/technology/language agnostic, as to work for all different implementations of GraphQL servers. As such, it would be implemented as an acceptance test, executing requests against the single endpoint of the GraphQL server’s running instance.

My GraphQL server is an implementation for WordPress. If I were to execute an acceptance test suite, I’d need to fire up a complete WordPress environment, including an instance of a MySQL DB. This, in turn, would add a lot of complexity to my GitHub Actions-based CI.

Instead, I rely on unit tests which completely mock the WordPress functionality, so not only I don’t need an actual MySQL instance, but also WordPress is not needed in order to run the tests, making them much simpler, faster and cheaper.

If the acceptance tests were available, I’d actually struggle to take advantage of them. Since the same validations are already being tested via the unit tests I’ve already created, then I wouldn’t really bother in using the acceptance tests.

A previous attempt to bring generic test suites to GraphQL, called Cats, similarly hit several issues that made the proposal impractical.

While the idea of a generic test suite is compelling, the effort required to pull it off does not appear worth it to me.

Conclusion

Would adding generic error codes to the GraphQL spec be worth it? My impression is yes, but not because of the availability of generic test suites (which is the reason stated in the original issue).

The benefits I’ve gained from supporting error codes are:

  • It allows the application to work with codes (which do not change), a unit that is more reliable than messages, which may be updated
  • It encourages the use of central locations to manage all error codes, which can give developers a better understanding of all validations to perform
  • It allows me to track which validations have not been implemented yet
  • It allows providing the extra specifiedBy information, pointing to online documentation for the error, at minimal cost
  • It allows providing other types of messages to the user, such as “warnings”, “suggestions” and “notices”, using the same logic

One drawback I’ve experienced is that looking at the logic performing the validations got more difficult to understand than before, because a meaningless error code does not explain what the error is about.

The other drawback I’ve suffered is the time and effort that was required for the transformation of my server’s codebase. The overall refactoring took around one month of work, while I felt like I was running in circles, spending energy just to arrive where I already was, constantly feeling I should instead invest my time in adding new features to the server.

But as a result of the migration, I now have the satisfaction of dealing with a sturdier codebase: unit tests have improved, new types of feedback messages were introduced (and other ones can be added at any moment with minimal effort), and documentation is returned to the user accessing the API. Overall, I feel that the quality of the API has gone up.

As the benefits outweigh the drawbacks, I’m convinced that adding generic error codes to the GraphQL spec is worth pursuing.

Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.LogRocket Dashboard Free Trial Bannerhttps://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries’ key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.

Понравилась статья? Поделить с друзьями:
  • Graphics rules maker sims 2 ошибка
  • Granta ошибка p1602
  • Granitebeard sea of thieves ошибка
  • Grandum3 ошибка что это
  • Grand vitara ошибка p0012