Skip to content

Conversation

@hitherejoe
Copy link

Description

Added section on modeling errors as data in GraphQL APIs, detailing recoverable and unrecoverable errors, and how to structure mutations and queries to handle errors effectively.

This has been added based off some conversations with @Urigo and some of the team at The Guild.

A couple of notes:

  • I added this as a new part of the Errors section. While the "Errors as Data" section is quite long, this felt like the best fit.
  • I've adjusted the examples to match the existing operations that are used in the page, happy to tweak things more if need-be!

Added section on modeling errors as data in GraphQL APIs, detailing recoverable and unrecoverable errors, and how to structure mutations and queries to handle errors effectively.
@vercel
Copy link

vercel bot commented Oct 29, 2025

@hitherejoe is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Oct 29, 2025

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: hitherejoe / name: Joe Birch (b7cded6)

Copy link
Contributor

@martinbonnin martinbonnin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super excited by this, thanks for looking into it.

A bunch of questions. Plus I'd love to see a section about caching considerations. What is cached vs non-cached.

Comment on lines +111 to +112
These are errors that are not the users fault (developer errors), which are generally things that the user can’t recover from. For example, the user not being authenticated or a resource not being found. This will also include scenarios such as
server crashes, unhandled exceptions and exhausted resources (for example, memory or CPU)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also a sub-dimension: is "exceptional" vs "persistent".

exceptional: if the client retries, it will most likely not get an error.

  • Out of memory error: irrecoverable && exceptional
  • Missing resource: irrecoverable && persistent

The persistent errors, you might want to model as data as well because then they become cacheable. So you don't do the same request over and over again.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about the "missing resource" case.

This sounds like "I query for Hero:123, and that record is missing". In this case, I'd normally return null.

Maybe I'm not thinking of the right scenario?


For example, the user could be querying for data that requires them to upgrade their plan, or to update their app. These are user-recoverable errors and utilising errors as data can improve both the Developer and User experience in these scenarios.

While this is a likely to not be common when implementing queries, this approach allows us to return user recoverable errors when required.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we think of this:

type Query {
  hero(id: ID!): Hero # error if not found
  hero(id: ID!): HeroPayload! # typed error if not found
}

I think (without any data to back it up) that hero(id: ID!): Hero is widely more used. But this is technically recoverable so hero(id: ID!): HeroPayload! might make more sense?

Copy link

@eddeee888 eddeee888 Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually go with the Payload approach in queries as well. As it gives clients the ability to handle partial error.


For example, let's say we want to query for two sets of heroes to compare side by side on a client, I'd write the operation like this:

query HeroCompare {
  heroSetOne: heroes(ids: ["1","2", "3", "4", "5"]) {
    id
    name
  }

  heroSetTwo: heroes(ids: "6","7", "8", "9", "10") {
    id
    name
  }
}

In this case:

  • If there's an error in either heroSetOne or heroSetTwo, the whole query will error and we'd get data: null
  • This could be disruptive for clients because we'd lose the ability to display the successful call

If we used the Payload approach though, if one fails (but properly handled in try/catch), the other may still complete and allows clients to handle partial failure

For example, this'd be the operation

query {
  heroSetOne: heroesPayload(input: ["1","2","3","4","5"]) {
    ... on HeroesPayloadOk {
      result {
        id
        name
      }
    }
    ... on ResultError {
      __typename
    }
  }

  heroSetTwo: heroesPayload(input: ["6","7","8","9","10"]) {
    ... on HeroesPayloadOk {
      result {
        id
        name
      }
    }
    ... on ResultError {
      __typename
    }
  }
}

And if heroSetOne fails we'd get something like this:

{
  "data": {
    "heroPayloadSetOne": {
      "__typename": "ResultError"
    },
    "heroPayloadSetTwo": {
      "result": [
        {
          "id": "6",
          "name": "Hero:6"
        },
        {
          "id": "7",
          "name": "Hero:7"
        },
        {
          "id": "8",
          "name": "Hero:8"
        },
        {
          "id": "9",
          "name": "Hero:9"
        },
        {
          "id": "10",
          "name": "Hero:10"
        }
      ]
    }
  }
}

Copy link

@eddeee888 eddeee888 Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also applicable to nested fields too.

Let's say we want to build a Hero details page. A Hero has Friends and Starships. Each may trigger requests to appropriate datasources:

type Hero {
  friends: HeroFriendsPayload!
  starships: HeroStarshipsPayload!
}

If either friends or starships fail, clients may still have the option to display the successful call, rather than seeing a full page error.


#### Mutations
##### Modelling Errors
Every mutation defined in the schema returns a Payload union - this allows mutations to return their success state, along with any user facing errors that may occur. This should follow the format of the mutation name, suffixed with Payload - e.g `{MutationName}Payload`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd root for {MutationName}Result personally but I have no idea what is used in the wild so I'd be happy to follow any existing pattern.

Whatever we decide, we should document it and make it a lint rule. See also graphql/graphiql#4000

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small technical nit: it's probably {FieldName}Result more than {MutationName}Result because MutationName usually refers to the name of the mutation in the executable document, not in the schema.

@Urigo Urigo requested review from benjie and enisdenjo October 29, 2025 15:04

```graphql
interface MutationError {
message: String!
Copy link

@eddeee888 eddeee888 Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm usually on the fence about servers returning errors as string:

  • If there were multiple clients (web, mobile), there's no guarantee this message would work for all of them e.g. some devices could be narrower so a long message won't work
  • A client could have more specific requirement e.g. timezone, translations, locale, etc.

So, usually I find my team bypassing the message and using the error type (either __typename or enum). This gives more power to the client to handle errors how they want, which can be more flexible (there's pros/cons of course)

##### Consuming Errors

When it comes to consuming typed errors, clients can use the ... on pattern to consume specific errors being returned in the response. In some cases, clients will want to know exactly what error has occurred and then use this to communicate some information to the user, as well as possibly show a specific user-path to recover from that error. When this is the case, clients can consume the typed error directly within the mutation.
Clients only need to consume the specific typed errors that they need to handle. For errors that fall outside of this required, the catch-all `... on MutationError` can be used to consume remaining errors in a generic fashion to communicate the given message to the user.
Copy link

@eddeee888 eddeee888 Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if we should go more specific how to consume different types of errors?
For example, I usually use __typename, but would mentioning something like that be too granular here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants