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.
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.
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
Since Apollo Server 4 removed built-in error classes, we'll create our own error handling system:
TypeScript1import { 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}
Let's implement error handling using our custom error classes:
TypeScript1import { 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};
Here's how to validate inputs using our custom error handling:
TypeScript1const 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};
Apollo Server 4 allows you to customize error formatting using the formatError
option:
TypeScript1const 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});
Here's a complete example putting it all together:
TypeScript1import { 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);
And here's how to make queries to the server:
TypeScript1import 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');
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!