iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Errors

GraphQL has two error channels: the protocol errors array (syntax, validation, unhandled exceptions) and user-facing errors you return as data (typed unions or result types). Mix them well and clients can write robust UI; mix them poorly and every screen ends up with try/catch around every field.

Typed errors as a union return type

EXAMPLE
# schema.graphql
type Mutation {
  signIn(email: String!, password: String!): SignInResult!
}

# A union of all the things signIn can return
union SignInResult = SignInSuccess | InvalidCredentials | AccountLocked | RateLimited

type SignInSuccess {
  token: String!
  user: User!
}

interface UserError {
  message: String!
  code: String!
}

type InvalidCredentials implements UserError {
  message: String!
  code: String!
}

type AccountLocked implements UserError {
  message: String!
  code: String!
  unlockAt: DateTime!
}

type RateLimited implements UserError {
  message: String!
  code: String!
  retryAfterSeconds: Int!
}

# --- resolver (Apollo Server / Yoga) ---
const resolvers = {
  Mutation: {
    async signIn(_, { email, password }, ctx) {
      const user = await ctx.users.byEmail(email);
      if (!user || !(await user.verify(password))) {
        return { __typename: 'InvalidCredentials',
          code: 'INVALID_CREDENTIALS', message: 'Email or password is wrong.' };
      }
      if (user.lockedUntil && user.lockedUntil > new Date()) {
        return { __typename: 'AccountLocked',
          code: 'ACCOUNT_LOCKED',
          message: 'Account is locked. Try again later.',
          unlockAt: user.lockedUntil };
      }
      if (await ctx.rateLimit.tooMany(user.id)) {
        return { __typename: 'RateLimited',
          code: 'RATE_LIMITED',
          message: 'Too many attempts.',
          retryAfterSeconds: 60 };
      }
      return { __typename: 'SignInSuccess',
        token: await ctx.tokens.issue(user), user };
    },
  },
};

# --- client query exhaustively handles every member ---
mutation SignIn($email: String!, $pw: String!) {
  signIn(email: $email, password: $pw) {
    __typename
    ... on SignInSuccess        { token user { id email } }
    ... on InvalidCredentials   { code message }
    ... on AccountLocked        { code message unlockAt }
    ... on RateLimited          { code message retryAfterSeconds }
  }
}

Why it matters

Reserve the top-level errors array for things the client cannot reasonably handle (network, schema, bug). Anything the UI must react to — wrong password, validation, payment declined — belongs in the data as a typed result. Then the client compiler can prove you handled every case.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// errors[] in the response carries problems even when partial data is present.
Try it Yourself »

Discussion

Loading…