Chapter 6: Error Handling & Validation
Proper error handling and validation are crucial for building robust GraphQL APIs. In this chapter, we'll explore how to handle errors gracefully and validate input data effectively.
Error Formatting
Custom Error Types
Create custom error classes for different types of errors:
import { GraphQLError } from 'graphql'
class ValidationError extends GraphQLError {
constructor(message: string, field?: string) {
super(message, {
extensions: {
code: 'VALIDATION_ERROR',
field,
http: { status: 400 }
}
})
}
}
class NotFoundError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 }
}
})
}
}
class AuthenticationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 }
}
})
}
}
class AuthorizationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'FORBIDDEN',
http: { status: 403 }
}
})
}
}
Error Formatting with GraphQL Yoga
Customize error formatting in GraphQL Yoga:
import { createYoga } from '@graphql-yoga/node'
const yoga = createYoga({
schema,
plugins: [
{
onValidate({ addValidationError }) {
return {
onValidateEnd({ valid, result }) {
if (!valid) {
result.errors?.forEach(error => {
addValidationError(error)
})
}
}
}
}
}
],
errorOptions: {
maskError: (error) => {
// Don't expose internal errors to clients
if (error.originalError instanceof Error) {
return new GraphQLError('Internal server error')
}
return error
}
}
})
Input Validation
Using Zod for Validation
Zod is a TypeScript-first schema validation library:
import { z } from 'zod'
// Define validation schemas
const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2)
})
const UpdateUserSchema = z.object({
email: z.string().email().optional(),
name: z.string().min(2).optional()
})
// Use in resolvers
const resolvers = {
Mutation: {
createUser: async (_, { input }, context) => {
// Validate input
const validatedInput = CreateUserSchema.parse(input)
// Create user with validated input
return context.prisma.user.create({
data: validatedInput
})
},
updateUser: async (_, { id, input }, context) => {
// Validate input
const validatedInput = UpdateUserSchema.parse(input)
// Update user with validated input
return context.prisma.user.update({
where: { id },
data: validatedInput
})
}
}
}
Custom Scalar Validations
Create custom scalars with validation:
import { GraphQLScalarType, Kind } from 'graphql'
import { z } from 'zod'
const EmailScalar = new GraphQLScalarType({
name: 'Email',
description: 'Email custom scalar type',
serialize(value) {
return value
},
parseValue(value) {
const result = z.string().email().safeParse(value)
if (!result.success) {
throw new ValidationError('Invalid email format')
}
return result.data
},
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
const result = z.string().email().safeParse(ast.value)
if (!result.success) {
throw new ValidationError('Invalid email format')
}
return result.data
}
throw new ValidationError('Email must be a string')
}
})
Error Handling Patterns
Try-Catch Pattern
const resolvers = {
Query: {
user: async (_, { id }, context) => {
try {
const user = await context.prisma.user.findUnique({
where: { id }
})
if (!user) {
throw new NotFoundError(`User with id ${id} not found`)
}
return user
} catch (error) {
if (error instanceof GraphQLError) {
throw error
}
// Log unexpected errors
console.error('Unexpected error:', error)
throw new GraphQLError('Internal server error')
}
}
}
}
Error Boundary Pattern
Create an error boundary to handle errors at a higher level:
const withErrorBoundary = (resolver: ResolverFn) => {
return async (parent, args, context, info) => {
try {
return await resolver(parent, args, context, info)
} catch (error) {
if (error instanceof GraphQLError) {
throw error
}
// Log unexpected errors
console.error('Unexpected error:', error)
// Map common errors to GraphQL errors
if (error.code === 'P2002') {
throw new ValidationError('A unique constraint would be violated')
}
throw new GraphQLError('Internal server error')
}
}
}
// Use in resolvers
const resolvers = {
Mutation: {
createUser: withErrorBoundary(async (_, { input }, context) => {
return context.prisma.user.create({
data: input
})
})
}
}
Logging and Monitoring
Error Logging
Implement comprehensive error logging:
import { createLogger, format, transports } from 'winston'
const logger = createLogger({
level: 'error',
format: format.combine(
format.timestamp(),
format.json()
),
transports: [
new transports.File({ filename: 'error.log' }),
new transports.Console()
]
})
const withLogging = (resolver: ResolverFn) => {
return async (parent, args, context, info) => {
try {
return await resolver(parent, args, context, info)
} catch (error) {
logger.error('Resolver error', {
error,
operation: info.operation.operation,
field: info.fieldName,
args,
context: {
userId: context.user?.id,
requestId: context.requestId
}
})
throw error
}
}
}
Performance Monitoring
Monitor resolver performance:
const withPerformanceMonitoring = (resolver: ResolverFn) => {
return async (parent, args, context, info) => {
const start = performance.now()
try {
const result = await resolver(parent, args, context, info)
const duration = performance.now() - start
// Log slow resolvers
if (duration > 1000) {
logger.warn('Slow resolver', {
operation: info.operation.operation,
field: info.fieldName,
duration
})
}
return result
} catch (error) {
const duration = performance.now() - start
logger.error('Resolver error', {
error,
operation: info.operation.operation,
field: info.fieldName,
duration
})
throw error
}
}
}
Example: Complete Error Handling
Here's a complete example of error handling in a GraphQL API:
// error.ts
export class ValidationError extends GraphQLError {
constructor(message: string, field?: string) {
super(message, {
extensions: {
code: 'VALIDATION_ERROR',
field,
http: { status: 400 }
}
})
}
}
// validation.ts
import { z } from 'zod'
export const CreatePostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
published: z.boolean().optional()
})
// middleware.ts
export const withErrorHandling = (resolver: ResolverFn) => {
return async (parent, args, context, info) => {
try {
return await resolver(parent, args, context, info)
} catch (error) {
if (error instanceof GraphQLError) {
throw error
}
logger.error('Unexpected error', {
error,
operation: info.operation.operation,
field: info.fieldName
})
throw new GraphQLError('Internal server error')
}
}
}
// resolvers.ts
const resolvers = {
Mutation: {
createPost: withErrorHandling(async (_, { input }, context) => {
// Validate input
const validatedInput = CreatePostSchema.parse(input)
// Check authorization
if (!context.user) {
throw new AuthenticationError('You must be logged in')
}
// Create post
return context.prisma.post.create({
data: {
...validatedInput,
authorId: context.user.id
}
})
})
}
}
// server.ts
const yoga = createYoga({
schema,
plugins: [
{
onValidate({ addValidationError }) {
return {
onValidateEnd({ valid, result }) {
if (!valid) {
result.errors?.forEach(error => {
addValidationError(error)
})
}
}
}
}
}
],
errorOptions: {
maskError: (error) => {
if (error.originalError instanceof Error) {
return new GraphQLError('Internal server error')
}
return error
}
}
})
Summary
In this chapter, we've covered:
- Custom error types and formatting
- Input validation with Zod
- Error handling patterns
- Logging and monitoring
- Best practices for error handling
These patterns will help you build robust and maintainable GraphQL APIs. In the next chapter, we'll explore real-time updates with subscriptions.