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
and TypeScript
. Proper error handling is crucial for building reliable and user-friendly applications.
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
Apollo Server
provides built-in error classes like UserInputError
to help you handle common errors. Let's start with handling missing data and validating user inputs.
Here's how you can throw a UserInputError
if a book is not found:
TypeScript1import { ApolloServer, gql, UserInputError } from 'apollo-server'; 2 3const books = [ 4 { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' }, 5 { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' } 6]; 7 8const resolvers = { 9 Query: { 10 book: (_: any, { id }: { id: string }) => { 11 const book = books.find(book => book.id === id); 12 if (!book) { 13 throw new UserInputError('Book not found'); 14 } 15 return book; 16 }, 17 } 18};
In this code, when a book with the specified ID is not found, a UserInputError
is thrown with the message "Book not found."
You can also throw an error if the required inputs are missing:
TypeScript1const resolvers = { 2 Mutation: { 3 addBook: (_: any, { title, author }: { title: string; author: string }) => { 4 if (!title || !author) { 5 throw new UserInputError('Title and Author are required'); 6 } 7 const newBook = { id: String(books.length + 1), title, author }; 8 books.push(newBook); 9 return newBook; 10 }, 11 } 12};
In this code, if either title
or author
is missing, a UserInputError
is thrown with a relevant message.
For more complex cases, you might want to create custom error classes:
TypeScript1class MyCustomError extends Error { 2 constructor(message: string) { 3 super(message); 4 this.name = 'MyCustomError'; 5 } 6}
You can handle multiple errors by creating a list of errors and throwing them as needed:
TypeScript1const errors = []; 2if (!title) errors.push('Title is required'); 3if (!author) errors.push('Author is required'); 4if (errors.length > 0) throw new UserInputError(errors.join(', '));
Now, let's put it all together in a complete example. We'll use the provided outcome code:
Here is our server code:
TypeScript1import { ApolloServer, gql, UserInputError } from 'apollo-server'; 2 3const typeDefs = gql` 4 type Book { 5 id: ID! 6 title: String! 7 author: String! 8 } 9 10 type Query { 11 book(id: ID!): Book 12 } 13 14 type Mutation { 15 addBook(title: String!, author: String!): Book 16 } 17`; 18 19const books = [ 20 { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' }, 21 { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' } 22]; 23 24const resolvers = { 25 Query: { 26 book: (_: any, { id }: { id: string }) => { 27 const book = books.find(book => book.id === id); 28 if (!book) { 29 throw new UserInputError('Book not found'); 30 } 31 return book; 32 }, 33 }, 34 Mutation: { 35 addBook: (_: any, { title, author }: { title: string; author: string }) => { 36 if (!title || !author) { 37 throw new UserInputError('Title and Author are required'); 38 } 39 const newBook = { id: String(books.length + 1), title, author }; 40 books.push(newBook); 41 return newBook; 42 }, 43 } 44}; 45 46const server = new ApolloServer({ typeDefs, resolvers }); 47 48server.listen().then(({ url }) => { 49 console.log(`🚀 Server ready at ${url}`); 50});
And here is how we make queries for the server:
TypeScript1import fetch from 'node-fetch'; 2 3const url = 'http://localhost:4000/'; 4 5const fetchBook = async (id: string) => { 6 const query = ` 7 query { 8 book(id: "${id}") { 9 title 10 author 11 } 12 } 13 `; 14 15 try { 16 const response = await fetch(url, { 17 method: 'POST', 18 headers: { 19 'Content-Type': 'application/json', 20 }, 21 body: JSON.stringify({ 22 query, 23 }), 24 }); 25 26 const data = await response.json(); 27 28 if (data.errors) { 29 console.error('Errors:', data.errors); 30 } else { 31 console.log('Data:', data); 32 } 33 } catch (error) { 34 console.error('Network Error:', error); 35 } 36}; 37 38const addNewBook = async (title: string, author: string) => { 39 const mutation = ` 40 mutation { 41 addBook(title: "${title}", author: "${author}") { 42 id 43 title 44 author 45 } 46 } 47 `; 48 49 try { 50 const response = await fetch(url, { 51 method: 'POST', 52 headers: { 53 'Content-Type': 'application/json', 54 }, 55 body: JSON.stringify({ 56 query: mutation, 57 }), 58 }); 59 60 const data = await response.json(); 61 62 if (data.errors) { 63 console.error('Errors:', data.errors); 64 } else { 65 console.log('Data:', data); 66 } 67 } catch (error) { 68 console.error('Network Error:', error); 69 } 70}; 71 72// Test the functions 73fetchBook('1'); 74addNewBook('1984', 'George Orwell');
In this code, errors from the server response are logged to the console using console.error
. Additionally, any network errors during the fetch request are caught and logged as "Network Error".
When you run this code, you will set up a server that handles both queries and mutations with proper error handling.
To summarize, in this lesson, you learned the importance of error handling in GraphQL, how to implement basic and advanced error handling techniques, and saw how to apply them in a complete application.
As you move on to the practice exercises, apply these techniques to solidify your understanding. Congratulations on completing this lesson and the course! You've gained essential skills to develop secure and resilient GraphQL APIs. Keep practicing and exploring more advanced topics to enhance your expertise.