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, всё-таки у Саши лучше

Status Codes and Error Responses

GraphQL API queries return HTTP status codes or error responses that describe the query status.

Success with HTTP POST request. The response contains the query result. GraphQL API can also return a 200 OK in cases where the request contains an error, such as an invalid object name or field. The error is represented by the GraphQLErrorRepresentation type.

A 200 OK status code is returned even if the query doesn’t return any matching items or if you pass in an invalid record ID.

The ErrorType field returns one of these errors.

  • InvalidSyntax — The request contains a syntax error.
  • ValidationError — The request isn’t valid against the GraphQL API schema. For example, this error can occur if an object or field name included in the query doesn’t exist.
  • DataFetchingException — An exception occurred while fetching data, for example, invalid credentials.
  • OperationNotSupported — Although the request is valid against the GraphQL API schema and successfully parsed, the server can’t resolve which operation to run.
  • ExecutionAborted — The request didn’t complete successfully.

The 4XX status codes are less common for GraphQL. They can signify issues with your network or with your connection to a Salesforce org.

401 Unauthorized

The session ID or OAuth token has expired or is invalid. For more information on the error, review the message and errorCode descriptions in the response body.

404 Not Found

The specified endpoint wasn’t found. Only the graphql endpoint is supported.

GraphQL API usually returns a 200 OK status code even if a record isn’t found or is deleted. Contrastingly, UI API can return a 404 error when a resource isn’t found or is deleted. See the 200 OK section above for details.

5XX status codes usually mean there’s an internal Salesforce error. To resolve this issue, try your request again or contact Salesforce Customer Support if the issue persists.

500 Internal Server Error

An error has occurred within Lightning Platform, so the request could not be completed.

503 Service Unavailable

Too many requests in an hour or the server is down for maintenance. See Rate Limiting.

Apollo Client can encounter a variety of errors when executing operations on your GraphQL server. Apollo Client helps you handle these errors according to their type, enabling you to show appropriate information to the user when an error occurs.

Error types

Executing GraphQL operations on a remote server can result in GraphQL errors or network errors.

GraphQL errors

These are errors related to the server-side execution of a GraphQL operation. They include:

  • Syntax errors (e.g., a query was malformed)
  • Validation errors (e.g., a query included a schema field that doesn’t exist)
  • Resolver errors (e.g., an error occurred while attempting to populate a query field)

If a syntax error or validation error occurs, your server doesn’t execute the operation at all because it’s invalid. If resolver errors occur, your server can still return partial data.

If a GraphQL error occurs, your server includes it in the errors array of its response to Apollo Client:

Apollo Client then adds those errors to the error.graphQLErrors array returned by your useQuery call (or whichever operation hook you used).

If a GraphQL error prevents Apollo Server from executing your operation at all, it responds with a 4xx status code. Apollo Server responds with a 200 status code if resolver errors occurred but the response still includes partial data.

Partial data with resolver errors

An operation that produces resolver errors might also return partial data. This means that some (but not all) of the data your operation requested is included in your server’s response. Apollo Client ignores partial data by default, but you can override this behavior by setting a GraphQL error policy.

Network errors

These are errors encountered while attempting to communicate with your GraphQL server, usually resulting in a 4xx or 5xx response status code (and no data).

When a network error occurs, Apollo Client adds it to the error.networkError field returned by your useQuery call (or whichever operation hook you used).

You can add retry logic and other advanced network error handling to your application with Apollo Link.

GraphQL error policies

If a GraphQL operation produces one or more resolver errors, your server’s response might still include partial data in the data field:

"message": "Failed to get string!"

By default, Apollo Client throws away partial data and populates the error.graphQLErrors array of your useQuery call (or whichever hook you’re using). You can instead use these partial results by defining an error policy for your operation.

Apollo Client supports the following error policies for an operation:

Policy Description
none If the response includes GraphQL errors, they are returned on error.graphQLErrors and the response data is set to undefined even if the server returns data in its response. This means network errors and GraphQL errors result in a similar response shape. This is the default error policy.
ignore graphQLErrors are ignored (error.graphQLErrors is not populated), and any returned data is cached and rendered as if no errors occurred.
all Both data and error.graphQLErrors are populated, enabling you to render both partial results and error information.

Setting an error policy

Specify an error policy in the options object you provide your operation hook (such as useQuery), like so:

function ShowingSomeErrors() {

const { loading, error, data } = useQuery(MY_QUERY, { errorPolicy: "all" });

if (loading) return <span>loading...</span>;

<h2>Good: {data.goodField}</h2>

{error.graphQLErrors.map(({ message }, i) => (

<span key={i}>{message}</span>

This example uses the all error policy to render both partial data and error information whenever applicable.

Advanced error handling with Apollo Link

The Apollo Link library enables you to configure advanced handling of errors that occur while executing GraphQL operations.

As a recommended first step, you can add an onError link to your link chain that receives error details and acts on them accordingly.

The example below passes the ApolloClient constructor a link chain with two links:

  • An onError link that checks for graphQLErrors or a networkError in the server’s response. It logs the details of whichever error(s) it finds.
  • An HttpLink that sends each GraphQL operation to your server.
    • This is the chain’s terminating link.

Retrying operations

Apollo Link helps you retry failed operations that might be resolved by a followup attempt. We recommend different links depending on the type of error that occurred:

  • The onError link for GraphQL errors
  • The RetryLink for network errors

On GraphQL errors

The onError link can retry a failed operation based on the type of GraphQL error that’s returned. For example, when using token-based authentication, you might want to automatically handle re-authentication when the token expires.

To retry an operation, you return forward(operation) in your onError function. Here’s an example:

onError(({ graphQLErrors, networkError, operation, forward }) => {

for (let err of graphQLErrors) {

switch (err.extensions.code) {

const oldHeaders = operation.getContext().headers;

authorization: getNewToken(),

return forward(operation);

console.log(`[Network error]: ${networkError}`);

If your retried operation also results in errors, those errors are not passed to your onError link to prevent an infinite loop of operations. This means that an onError link can retry a particular operation only once.

If you don’t want to retry an operation, your onError link’s function should return nothing.

On network errors

To retry operations that encounter a network error, we recommend adding a RetryLink to your link chain. This link enables you to configure retry logic like exponential backoff and total number of attempts.

See the documentation for RetryLink.

onError link options

See the onError API reference.

GraphQL has become a popular choice for building modern web APIs due to its flexibility and efficiency. However, GraphQL APIs are not immune to errors like any other technology.

Errors can occur at different stages of a GraphQL operation, including during query/mutation validation, syntax parsing, and resolver execution.

In this article, we will take a closer look at the different types of GraphQL errors, namely syntax, validation, and resolver errors, and discuss best practices for handling them effectively.

GraphQL Errors

GraphQL APIs are defined using a schema that describes the available types, fields, and operations. Clients can then send queries, mutations, and subscriptions to the server, and the server responds with the requested data.

However, errors can occur when dealing with complex queries and mutations, impacting the reliability and performance of GraphQL APIs.

Handling errors in GraphQL APIs is crucial to ensure smooth operation and provide a good user experience. Errors can arise from various sources, including incorrect syntax in queries or mutations, validation errors due to mismatched types or missing arguments, and resolver errors that occur during the execution of a query or mutation.

Let’s take a closer look at these types of errors and explore best practices for handling them effectively.

Syntax Errors

Syntax errors in GraphQL occur when the query or mutation does not follow the correct syntax rules defined by the GraphQL specification. These errors can prevent the query or mutation from being parsed and executed by the server.

Some examples of syntax errors in GraphQL queries and mutations include missing or misplaced brackets, incorrect field names, and invalid argument values. For instance, a missing closing bracket or a typo in a field name can result in a syntax error that prevents the query from being executed.

Here’s an example of an error response for a syntax error in a GraphQL query:

{

"errors": [

{

"message": "[line: 1] field 'description' is not defined in 'Project'"

}

],

"data": null,

"extensions": {

"requestId": "clgjsamqwcyns0bmk72uuth88"

}

}

The best way to prevent syntax errors is to familiarize yourself with the GraphQL syntax and follow best practices when writing queries and mutations.

Using a GraphQL client or IDE that provides syntax highlighting and validation is also important for catching syntax errors in real-time. Additionally, thorough testing and code review can help identify and fix syntax errors before production deployment.

When a syntax error occurs, the server typically returns an error response with a specific error message indicating the syntax issue. It’s important to provide informative error messages that can help developers identify and fix the syntax error quickly.

Validation Errors

Validation errors in GraphQL occur when a query or mutation fails to validate against the defined schema.

GraphQL schemas define the types, fields, and operations available in the API, and queries or mutations must adhere to these definitions to be considered valid.

Validation errors can arise from various sources, including missing or incorrect arguments, invalid enum values, and incompatible types. For example, a validation error will occur if a required argument is missing or has the wrong type. Similarly, a validation error will be triggered if an enum value provided in a query or mutation does not match the defined enum values.

Here’s an example of an error response for a validation error in a GraphQL mutation:

{

"errors": [

{

"message": "Variable \"$name\" of required type \"String!\" was not provided.",

"locations": [

{

"line": 3,

"column": 15

}

],

"extensions": {

"code": "GRAPHQL_VALIDATION_FAILED",

"fieldName": "createUser",

"argumentName": "name"

}

}

]

}

To minimize validation errors, it’s important to design a well-defined and robust GraphQL schema. This includes providing clear documentation for types, fields, and operations, defining clear validation rules for arguments, and using enums and custom scalars wherever applicable.

When a validation error occurs, the server typically returns an error response with a specific message indicating the validation issue. It’s crucial to provide meaningful error messages to help developers quickly identify and fix the validation error.

Resolver Errors

Resolver errors in GraphQL occur during the execution of a query or mutation when the resolver function encounters an error.

Resolvers are responsible for fetching data from various sources, such as databases, APIs, or other services, and returning the requested data to the client.

Resolver errors can arise from various sources, including database, network, and business logic errors. They can significantly impact the reliability and performance of GraphQL APIs. They can also result in incomplete or inconsistent data being returned to the client or cause the entire query or mutation to fail.

Here’s an example of an error response for a resolver error in a GraphQL query:

{

"errors": [

{

"message": "Error: Failed to fetch user data from the database.",

"locations": [

{

"line": 5,

"column": 10

}

],

"extensions": {

"code": "INTERNAL_SERVER_ERROR",

"fieldName": "getUser"

}

}

]

}

When a resolver error occurs, the server typically returns an error response with a specific error message indicating the issue. It’s essential to provide informative error messages to help developers quickly identify and fix the resolver error.

To handle resolver errors effectively, it’s important to implement proper error handling in resolver functions. This includes using try-catch blocks or error-handling middleware to capture and handle errors gracefully.

Logging and monitoring can help identify and resolve resolver errors in real-time, promptly addressing any issues.

Best Practices for Handling GraphQL Errors

Handling errors effectively in GraphQL APIs is crucial to ensure smooth operation and provide a good user experience. Here are some best practices for handling GraphQL errors:

  1. Provide meaningful error messages: Error messages should be informative and clearly indicate the issue, including details such as the error type, location, and relevant field or argument names. This can help developers identify and fix errors quickly.

  2. Use error extensions: GraphQL allows for custom error extensions, such as error codes or additional metadata, to be included in error responses. This can provide more context about the error and help with error handling and troubleshooting.

  3. Implement validation in the schema: Define clear validation rules in the GraphQL schema, including required arguments, allowed enum values, and custom validation logic. This can help catch validation errors before the query or mutation is executed.

  4. Implement proper error handling in resolvers: Resolvers should include proper error handling, such as try-catch blocks or error-handling middleware, to capture and handle errors gracefully. This can help prevent resolver errors from impacting the overall performance and reliability of the API.

  5. Use logging and monitoring: Implement logging and monitoring mechanisms in your GraphQL API to capture and track errors in real time. This can help identify and resolve issues quickly, ensuring smooth operation and optimal performance.

  6. Follow GraphQL conventions: Follow GraphQL conventions for error handling, such as using the «errors» field in the response to include error information. This can ensure consistency and interoperability across different GraphQL implementations.

  7. Handle sensitive information securely: Avoid including sensitive information in error messages or logs, such as database connection strings or access tokens. Handle sensitive information securely to protect your APIs and their user’s privacy and security.

  8. Document error handling: Document the error handling strategy and guidelines for your GraphQL API, including error codes, error messages, and recommended actions for developers. This can help troubleshoot and make it easier for developers to understand and handle errors in your API.

  9. Test error scenarios: Test your GraphQL API with different error scenarios, including invalid queries or mutations, missing arguments, and error-prone situations. This can help uncover potential issues and ensure that error-handling mechanisms work as expected.

  10. Provide feedback to clients: When returning error responses to clients, provide feedback on how to fix the error or suggest alternative actions. This can help developers understand and resolve issues quickly, improving the developer experience with your API.

Conclusion

Effective error handling is crucial for ensuring the reliability and performance of GraphQL APIs.

By following best practices such as providing meaningful error messages, implementing validation in the schema, and handling errors gracefully in resolvers, you can create robust and user-friendly GraphQL APIs.

Remember, error handling is not an afterthought but an integral part of building high-quality GraphQL APIs that deliver exceptional performance and user satisfaction.

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} ("PoP\ComponentModel") 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 "PoP\ComponentModel\e24" 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 "module1\1" and "module2\1" 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.

Понравилась статья? Поделить с друзьями:
  • Gs 05 ошибка мерседес актрос gs05
  • Granny2 dll ошибка
  • Gorenje ошибка e23
  • Gorenje коды ошибок варочная панель
  • Grandini ошибка e2