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…