git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/graphql-error-handling" ~/.claude/skills/intense-visions-harness-engineering-graphql-error-handling && rm -rf "$T"
agents/skills/claude-code/graphql-error-handling/SKILL.mdGraphQL Error Handling
Handle errors in GraphQL APIs with structured error types, result unions, and server-side error formatting
When to Use
- Designing error responses for a GraphQL API
- Choosing between top-level errors and typed error payloads
- Implementing validation errors for mutations
- Sanitizing error details before sending to clients
- Building error handling patterns that work across client and server
Instructions
-
Distinguish between two error categories. GraphQL has two channels for errors — treat them differently:
- Top-level
array: For unexpected system errors (database down, null in non-null field, resolver throws). These are infrastructure problems the client cannot fix.errors - Typed error fields in payloads: For expected domain errors (validation failures, business rule violations, not found). These are user-actionable.
- Top-level
-
Use a
type in mutation payloads. This is the standard pattern for returning actionable errors alongside successful results.UserError
type UserError { field: [String!] message: String! code: ErrorCode! } enum ErrorCode { VALIDATION_FAILED NOT_FOUND FORBIDDEN CONFLICT RATE_LIMITED } type CreateOrderPayload { order: Order errors: [UserError!]! }
- Use union result types for operations with distinct error states. When different error types carry different data, model them as a union.
union CreateOrderResult = CreateOrderSuccess | ValidationError | InsufficientStockError type CreateOrderSuccess { order: Order! } type ValidationError { field: String! message: String! } type InsufficientStockError { productId: ID! available: Int! requested: Int! }
Clients use
__typename to discriminate:
const { data } = useMutation(CREATE_ORDER); if (data.createOrder.__typename === 'CreateOrderSuccess') { // handle success } else if (data.createOrder.__typename === 'InsufficientStockError') { // show "Only X available" }
- Throw
with extensions for system errors. When something genuinely fails, throw aGraphQLError
with a code inGraphQLError
to help clients categorize the error.extensions
import { GraphQLError } from 'graphql'; throw new GraphQLError('Not authorized to view this resource', { extensions: { code: 'FORBIDDEN', http: { status: 403 }, }, });
- Use
on the server to sanitize outgoing errors. Strip stack traces, internal messages, and sensitive details in production. Log the full error server-side.formatError
const server = new ApolloServer({ typeDefs, resolvers, formatError: (formattedError, error) => { // Log the raw error for debugging logger.error(error); // Strip internal details in production if (process.env.NODE_ENV === 'production') { if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') { return { message: 'An unexpected error occurred', extensions: { code: 'INTERNAL_SERVER_ERROR' }, }; } delete formattedError.extensions?.stacktrace; } return formattedError; }, });
-
Never expose database errors, stack traces, or file paths to clients. These leak implementation details and aid attackers. Always map internal errors to generic messages.
-
Implement error boundary resolvers for non-null fields. If a non-null field resolver fails, the error propagates up to the nearest nullable parent, potentially nullifying large portions of the response. Place nullable "firewalls" at strategic points.
type Query { # If user resolver fails, only this field is null — not the entire response user(id: ID!): User # If feed fails, the entire Query becomes an error (bad) feed: [Post!]! }
- On the client, handle both error channels. Check
from the hook for network/system errors, and check the response payload for domain errors.error
const [createOrder, { error: networkError }] = useMutation(CREATE_ORDER); const result = await createOrder({ variables: { input } }); if (networkError) { // System error — show generic message } if (result.data?.createOrder.errors.length) { // Domain error — show field-specific validation messages }
Details
Partial data with errors: GraphQL can return both
data and errors in the same response. A query requesting three fields may return data for two and an error for one. Clients must handle this — do not treat any error as a total failure.
Error codes convention: Apollo defines standard codes:
GRAPHQL_PARSE_FAILED, GRAPHQL_VALIDATION_FAILED, BAD_USER_INPUT, UNAUTHENTICATED, FORBIDDEN, INTERNAL_SERVER_ERROR. Use these for consistency, and add custom codes for domain-specific errors.
Errors in lists: If a list field is
[User!]! and one user resolver fails, the entire list becomes null (bubbling up from the non-null item). Use [User]! if individual items can fail without destroying the whole list.
Testing error paths: Write tests that specifically verify error responses — both the structure (correct code, field paths) and the absence of sensitive data (no stack traces in production mode).
Source
https://www.apollographql.com/docs/apollo-server/data/errors/
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
- related_skills: graphql-resolver-pattern, graphql-schema-design, graphql-apollo-server, api-error-contracts, api-problem-details-rfc
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.