Lesson 4
Best Practices for Error Handling in GraphQL with Apollo Server 4
Deprecation Notice

This course path teaches Apollo Server 3, which has been deprecated. To access the new course paths covering Apollo Server 4, use this course path.

Introduction

Welcome to the lesson on Best Practices for Error Handling in GraphQL. In this lesson, we will explore how to handle errors effectively in your GraphQL API using Apollo Server 4 and TypeScript. Proper error handling is crucial for building reliable and user-friendly applications.

How GraphQL Handles Errors and Common Error Types

In REST API, a single error usually results in the entire request failing with an HTTP error status. However, In GraphQL, partial success is possible because errors are isolated to specific fields.

GraphQL treats errors as part of the response format. If any field in a query fails, it includes an errors array in the response. Common error types include:

  • User Input Errors
  • Authentication Errors
  • Validation Errors
  • System Errors
Creating Custom Error Classes

Since Apollo Server 4 removed built-in error classes, we'll create our own error handling system:

TypeScript
1import { GraphQLError } from 'graphql'; 2 3export class ValidationError extends GraphQLError { 4 constructor(message: string) { 5 super(message, { 6 extensions: { 7 code: 'BAD_USER_INPUT', 8 http: { status: 400 } 9 } 10 }); 11 } 12} 13 14export class NotFoundError extends GraphQLError { 15 constructor(message: string) { 16 super(message, { 17 extensions: { 18 code: 'NOT_FOUND', 19 http: { status: 404 } 20 } 21 }); 22 } 23} 24 25export class AuthenticationError extends GraphQLError { 26 constructor(message: string) { 27 super(message, { 28 extensions: { 29 code: 'UNAUTHENTICATED', 30 http: { status: 401 } 31 } 32 }); 33 } 34}
Implementing Basic Error Handling in Resolvers

Let's implement error handling using our custom error classes:

TypeScript
1import { ApolloServer } from '@apollo/server'; 2import { startStandaloneServer } from '@apollo/server/standalone'; 3 4interface Book { 5 id: string; 6 title: string; 7 author: string; 8} 9 10const books: Book[] = [ 11 { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' }, 12 { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' } 13]; 14 15const resolvers = { 16 Query: { 17 book: (_: unknown, { id }: { id: string }) => { 18 const book = books.find(book => book.id === id); 19 if (!book) { 20 throw new NotFoundError(`Book with ID ${id} not found`); 21 } 22 return book; 23 }, 24 } 25};
Validating User Inputs

Here's how to validate inputs using our custom error handling:

TypeScript
1const resolvers = { 2 Mutation: { 3 addBook: (_: unknown, { title, author }: { title: string; author: string }) => { 4 const validationErrors: string[] = []; 5 6 if (!title?.trim()) { 7 validationErrors.push('Title is required'); 8 } 9 if (!author?.trim()) { 10 validationErrors.push('Author is required'); 11 } 12 13 if (validationErrors.length > 0) { 14 throw new ValidationError(validationErrors.join(', ')); 15 } 16 17 const newBook = { 18 id: String(books.length + 1), 19 title: title.trim(), 20 author: author.trim() 21 }; 22 books.push(newBook); 23 return newBook; 24 }, 25 } 26};
Handling Multiple Errors with formatError

Apollo Server 4 allows you to customize error formatting using the formatError option:

TypeScript
1const server = new ApolloServer({ 2 typeDefs, 3 resolvers, 4 formatError: (formattedError, error) => { 5 // Log the error internally 6 console.error('GraphQL Error:', error); 7 8 // Don't expose internal server errors to the client 9 if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') { 10 return new GraphQLError('Internal server error', { 11 extensions: { 12 code: 'INTERNAL_SERVER_ERROR', 13 http: { status: 500 } 14 } 15 }); 16 } 17 18 return formattedError; 19 }, 20});
Complete Application Example

Here's a complete example putting it all together:

TypeScript
1import { ApolloServer } from '@apollo/server'; 2import { startStandaloneServer } from '@apollo/server/standalone'; 3import { GraphQLError } from 'graphql'; 4 5// Type definitions 6const typeDefs = `#graphql 7 type Book { 8 id: ID! 9 title: String! 10 author: String! 11 } 12 13 type Query { 14 book(id: ID!): Book 15 } 16 17 type Mutation { 18 addBook(title: String!, author: String!): Book 19 } 20`; 21 22// Custom error classes 23class ValidationError extends GraphQLError { 24 constructor(message: string) { 25 super(message, { 26 extensions: { 27 code: 'BAD_USER_INPUT', 28 http: { status: 400 } 29 } 30 }); 31 } 32} 33 34class NotFoundError extends GraphQLError { 35 constructor(message: string) { 36 super(message, { 37 extensions: { 38 code: 'NOT_FOUND', 39 http: { status: 404 } 40 } 41 }); 42 } 43} 44 45// Data store 46interface Book { 47 id: string; 48 title: string; 49 author: string; 50} 51 52const books: Book[] = [ 53 { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' }, 54 { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' } 55]; 56 57// Resolvers 58const resolvers = { 59 Query: { 60 book: (_: unknown, { id }: { id: string }) => { 61 const book = books.find(book => book.id === id); 62 if (!book) { 63 throw new NotFoundError(`Book with ID ${id} not found`); 64 } 65 return book; 66 }, 67 }, 68 Mutation: { 69 addBook: (_: unknown, { title, author }: { title: string; author: string }) => { 70 const validationErrors: string[] = []; 71 72 if (!title?.trim()) { 73 validationErrors.push('Title is required'); 74 } 75 if (!author?.trim()) { 76 validationErrors.push('Author is required'); 77 } 78 79 if (validationErrors.length > 0) { 80 throw new ValidationError(validationErrors.join(', ')); 81 } 82 83 const newBook = { 84 id: String(books.length + 1), 85 title: title.trim(), 86 author: author.trim() 87 }; 88 books.push(newBook); 89 return newBook; 90 }, 91 } 92}; 93 94// Server setup 95const server = new ApolloServer({ 96 typeDefs, 97 resolvers, 98 formatError: (formattedError, error) => { 99 console.error('GraphQL Error:', error); 100 101 if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') { 102 return new GraphQLError('Internal server error', { 103 extensions: { 104 code: 'INTERNAL_SERVER_ERROR', 105 http: { status: 500 } 106 } 107 }); 108 } 109 110 return formattedError; 111 }, 112}); 113 114const startServer = async () => { 115 const { url } = await startStandaloneServer(server, { 116 listen: { port: 4000 } 117 }); 118 console.log(`🚀 Server ready at ${url}`); 119}; 120 121startServer().catch(console.error);
Queries Example

And here's how to make queries to the server:

TypeScript
1import fetch from 'node-fetch'; 2 3const url = 'http://localhost:4000/'; 4 5interface GraphQLResponse<T> { 6 data?: T; 7 errors?: Array<{ 8 message: string; 9 extensions?: { 10 code: string; 11 }; 12 }>; 13} 14 15const fetchBook = async (id: string) => { 16 const query = ` 17 query GetBook($id: ID!) { 18 book(id: $id) { 19 title 20 author 21 } 22 } 23 `; 24 25 try { 26 const response = await fetch(url, { 27 method: 'POST', 28 headers: { 29 'Content-Type': 'application/json', 30 }, 31 body: JSON.stringify({ 32 query, 33 variables: { id }, 34 }), 35 }); 36 37 const data: GraphQLResponse<{ book: { title: string; author: string } }> = 38 await response.json(); 39 40 if (data.errors) { 41 console.error('GraphQL Errors:', data.errors); 42 return; 43 } 44 45 console.log('Book data:', data.data); 46 } catch (error) { 47 console.error('Network Error:', error); 48 } 49}; 50 51const addNewBook = async (title: string, author: string) => { 52 const mutation = ` 53 mutation AddBook($title: String!, $author: String!) { 54 addBook(title: $title, author: $author) { 55 id 56 title 57 author 58 } 59 } 60 `; 61 62 try { 63 const response = await fetch(url, { 64 method: 'POST', 65 headers: { 66 'Content-Type': 'application/json', 67 }, 68 body: JSON.stringify({ 69 query: mutation, 70 variables: { title, author }, 71 }), 72 }); 73 74 const data: GraphQLResponse<{ addBook: { id: string; title: string; author: string } }> = 75 await response.json(); 76 77 if (data.errors) { 78 console.error('GraphQL Errors:', data.errors); 79 return; 80 } 81 82 console.log('New book added:', data.data); 83 } catch (error) { 84 console.error('Network Error:', error); 85 } 86}; 87 88// Test the functions 89fetchBook('1'); 90addNewBook('1984', 'George Orwell');
Lesson Summary

In this updated lesson, you learned:

  • How to create custom error classes in Apollo Server 4
  • Best practices for error handling and validation
  • How to use the formatError function to customize error responses
  • Type-safe approaches to both server and client-side code
  • Modern GraphQL patterns including proper variable usage

Remember that good error handling not only improves the developer experience but also helps build more reliable and maintainable applications. Keep practicing these patterns in your GraphQL APIs!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.