Skip to main content

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.