☕Error Handling in GraphQL With Hot Chocolate
Simplifying error handling in GraphQL APIs with unions and custom error types, improving your API's responses and user experience using dotnet and hot chocolate
Types of Errors
In GraphQL, errors can be categorized into three main types network errors, unintentional error responses, and intentional error responses. Network errors occur due to issues with the network connection, while unintentional errors are caused by and unspecified error. Intentional error responses, on the other hand, are explicitly returned in the response as they are defined in GraphQL SDL.
🟥Network Errors
Network errors occur when there is an issue with the network connection between the client and server. Examples include timeouts, connection resets, server parse error and invalid query syntax. Hot Chocolate will return non 200 request code.
🟩200 OK Errors
In HTTP, a 200 OK
response code indicates that a request was successful and the server has returned the requested information to the client.
However, a 200 OK
response in GraphQL means that the request was processed successfully, but it does not necessarily mean that the response contains no errors. Instead, the top level errors
field can contain a list of error objects, each describing a specific error that occurred during the request. This is because GraphQL allows partial responses with errors to be returned to the client.
We will mainly focus on 200 OK
errors in this article
🔭Query
The code defines two records, Book
and Author
, which are used to represent data about books and their authors. Book
has properties for the book’s Title
, Author
(an instance of the Author
class), and ReleaseDate
. The Author
class has a single property, Name
, which represents the author’s name. These records are used in a GraphQL API to expose data about books and their authors to clients.
Book.cs
public record Book(
string Title,
Author Author,
DateTimeOffset ReleaseDate
);
public record Book(
string Title,
Author Author,
DateTimeOffset ReleaseDate
);
Author.cs
public record Author(string Name);
public record Author(string Name);
Lets start with a simple query to get all the books
GetAllBook Query
query GetAllBook {
book {
title
releaseDate
author {
name
}
}
}
query GetAllBook {
book {
title
releaseDate
author {
name
}
}
}
GetAllBook Response
{
"data": {
"book": [
{
"title": "If You Give a Developer a Cookie",
"releaseDate": "2022-03-04T07:48:45.710Z",
"author": { "name": "Jon Skeet" }
},
{
"title": "The Cookie Monster Strikes Back",
"releaseDate": "2024-03-04T07:48:45.710Z",
"author": { "name": "Jon Skeet" }
}
]
}
}
{
"data": {
"book": [
{
"title": "If You Give a Developer a Cookie",
"releaseDate": "2022-03-04T07:48:45.710Z",
"author": { "name": "Jon Skeet" }
},
{
"title": "The Cookie Monster Strikes Back",
"releaseDate": "2024-03-04T07:48:45.710Z",
"author": { "name": "Jon Skeet" }
}
]
}
}
🏔️Top Level Errors
To enhance the API’s functionality, the code introduces a BookExtensions
class defines a method for getting a book’s rating and throws an error if the book is unpublished. This error is thrown to notify clients that the book is not yet available.
Rating.cs
public record Rating(int Average, int Total = 0)
public record Rating(int Average, int Total = 0)
GetRating.cs (throw)
[ExtendObjectType(typeof(Book))]
public class BookExtensions
{
public Rating? GetRating([Parent] Book book)
{
return book.ReleaseDate < DateTimeOffset.UtcNow
? new Rating(5)
: throw new Exception("Book is not published yet.");
}
}
[ExtendObjectType(typeof(Book))]
public class BookExtensions
{
public Rating? GetRating([Parent] Book book)
{
return book.ReleaseDate < DateTimeOffset.UtcNow
? new Rating(5)
: throw new Exception("Book is not published yet.");
}
}
BooksWithRating Query
query BooksWithRating {
book {
title
releaseDate
author {
name
}
rating {
average
total
}
}
}
query BooksWithRating {
book {
title
releaseDate
author {
name
}
rating {
average
total
}
}
}
In the response we can see that we did not get any rating
back for The Cookie Monster Strikes Back
and the error we threw gets returned as an object back in the top level errors list adjacent to the data
BooksWithRating Response
{
"errors": [
{
"message": "Unexpected Execution Error",
"locations": [{ "line": 34, "column": 5 }],
"path": ["book", 1, "rating"],
"extensions": {
"message": "Book is not published yet.",
"stackTrace": "..."
}
}
],
"data": {
"book": [
{
"title": "If You Give a Developer a Cookie",
"releaseDate": "2022-03-04T07:49:57.537Z",
"author": { "name": "Jon Skeet" },
"rating": { "average": 5, "total": 0 }
},
{
"title": "The Cookie Monster Strikes Back",
"releaseDate": "2024-03-04T07:49:57.537Z",
"author": { "name": "Jon Skeet" },
"rating": null
}
]
}
}
{
"errors": [
{
"message": "Unexpected Execution Error",
"locations": [{ "line": 34, "column": 5 }],
"path": ["book", 1, "rating"],
"extensions": {
"message": "Book is not published yet.",
"stackTrace": "..."
}
}
],
"data": {
"book": [
{
"title": "If You Give a Developer a Cookie",
"releaseDate": "2022-03-04T07:49:57.537Z",
"author": { "name": "Jon Skeet" },
"rating": { "average": 5, "total": 0 }
},
{
"title": "The Cookie Monster Strikes Back",
"releaseDate": "2024-03-04T07:49:57.537Z",
"author": { "name": "Jon Skeet" },
"rating": null
}
]
}
}
Default Error Handling
Drawbacks Of Top Level Error List
Dealing with an errors at the top level of the response can be challenging because it requires the client to process and understand multiple error objects returned in the response. Here are some reasons why dealing with an top level error list can be hard:
Complexity
It can be complex and difficult to understand. It can contain multiple error objects, each with its own error code, message, and other metadata. This can make it hard for developers to understand the specific issues that occurred during the request.
Consistency
It can be inconsistent, meaning that the format and structure of the error objects can vary depending on the server implementation. This can make it challenging for clients to parse and handle errors in a consistent and predictable manner.
Scalability
As a GraphQL API grows and becomes more complex, the number and complexity of errors that can occur during a request can also increase. This can make it challenging to handle and manage error list effectively.
🤝Union Results
Unions can be used to simplify error handling in GraphQL as explained in an article by Sasha Solomon. By defining a union type that includes both the expected response type (in this case, BookRating
) and the possible error types (such as BookUnpublishedError
), the API can return either a valid response or an error response in a single field. This approach eliminates the need for top level error list and allows clients to more easily handle and display errors. Additionally, custom error types (like BookUnpublishedError
) can provide clients with specific details about the error (like PreOrderDate
), which can help users understand and address the issue more effectively.
BookRating.cs
[UnionType]
public interface BookRating { }
public record Rating(int Average, int Total = 0) : BookRating { }
public record BookUnpublishedError(
DateTimeOffset PreOrderDate,
string Message = "Book is not published yet"
) : BookRating { }
[UnionType]
public interface BookRating { }
public record Rating(int Average, int Total = 0) : BookRating { }
public record BookUnpublishedError(
DateTimeOffset PreOrderDate,
string Message = "Book is not published yet"
) : BookRating { }
GetRating.cs (Union)
public BookRating GetRating([Parent] Book book)
{
return book.ReleaseDate < DateTimeOffset.UtcNow
? new Rating(5)
: new BookUnpublishedError(book.ReleaseDate);
}
public BookRating GetRating([Parent] Book book)
{
return book.ReleaseDate < DateTimeOffset.UtcNow
? new Rating(5)
: new BookUnpublishedError(book.ReleaseDate);
}
BooksWithRatingUnion Query
query BooksWithRatingUnion {
book {
title
releaseDate
author {
name
}
rating {
__typename
... on Rating {
average
total
}
... on BookUnpublishedError {
message
preOrderDate
}
}
}
}
query BooksWithRatingUnion {
book {
title
releaseDate
author {
name
}
rating {
__typename
... on Rating {
average
total
}
... on BookUnpublishedError {
message
preOrderDate
}
}
}
}
BooksWithRatingUnion Response
{
"data": {
"book": [
{
"title": "If You Give a Developer a Cookie",
"releaseDate": "2022-03-04T08:40:34.157Z",
"author": { "name": "Jon Skeet" },
"rating": { "__typename": "Rating", "average": 5, "total": 0 }
},
{
"title": "The Cookie Monster Strikes Back",
"releaseDate": "2024-03-04T08:40:34.157Z",
"author": { "name": "Jon Skeet" },
"rating": {
"__typename": "BookUnpublishedError",
"message": "Book is not published yet",
"preOrderDate": "2023-12-04T08:40:34.157Z"
}
}
]
}
}
{
"data": {
"book": [
{
"title": "If You Give a Developer a Cookie",
"releaseDate": "2022-03-04T08:40:34.157Z",
"author": { "name": "Jon Skeet" },
"rating": { "__typename": "Rating", "average": 5, "total": 0 }
},
{
"title": "The Cookie Monster Strikes Back",
"releaseDate": "2024-03-04T08:40:34.157Z",
"author": { "name": "Jon Skeet" },
"rating": {
"__typename": "BookUnpublishedError",
"message": "Book is not published yet",
"preOrderDate": "2023-12-04T08:40:34.157Z"
}
}
]
}
}
Query Response Diagram
🖊️Mutation
Hot Chocolate makes working with mutation conventions easy by providing a built-in set of features and tools that streamline the process of creating and structuring mutations in a GraphQL API.
Error Union List
With the mutation conventions in Hot Chocolate, you can easily create mutations that follow the stage 6a pattern outlined by Marc-Andre Giroux. This pattern involves keeping the resolver code clean of any error handling and using exceptions to indicate an error state. The field will then expose which exceptions are domain errors that should be included in the schema. Any other exceptions will still cause runtime errors. This approach simplifies the creation of mutations and ensures that errors are handled consistently and effectively.
After enabling the mutation conventions in Hot Chocolate we can specify the errors that can be returned by the mutation as attributes.
Add Mutation Conventions
service
.AddGraphQLServer()
.AddMutationConventions() //👈
service
.AddGraphQLServer()
.AddMutationConventions() //👈
InsufficientFund.cs
public class InsufficientFundException : Exception
{
public int CurrentBalance { get; set; }
public int RequiredAmount { get; set; }
public InsufficientFundException(
int currentBalance,
int requiredAmount
) : base($"Not Enough Money 💵")
{
CurrentBalance = currentBalance;
RequiredAmount = requiredAmount;
}
}
public class InsufficientFundException : Exception
{
public int CurrentBalance { get; set; }
public int RequiredAmount { get; set; }
public InsufficientFundException(
int currentBalance,
int requiredAmount
) : base($"Not Enough Money 💵")
{
CurrentBalance = currentBalance;
RequiredAmount = requiredAmount;
}
}
OutOfStock.cs
public class OutOfStockException : Exception
{
public Book? SuggestedBook { get; set; }
public DateTimeOffset? BackInStockDate { get; set; }
public OutOfStockException() : base($"Out Of Stock") { }
}
public class OutOfStockException : Exception
{
public Book? SuggestedBook { get; set; }
public DateTimeOffset? BackInStockDate { get; set; }
public OutOfStockException() : base($"Out Of Stock") { }
}
[Error<InsufficientFundException>]
[Error<OutOfStockException>]
public async Task<Book> BuyBook(string title) {
var exceptions = new List<Exception>();
if (OutOfStock)
exceptions.Add(
new OutOfStockException() {
SuggestedBook = new Book("Error Handling in GraphQL"),
BackInStockDate = DateTimeOffset.UtcNow.AddDays(5),
}
);
if (InsufficientFund)
exceptions.Add(new InsufficientFundException(10, 50));
if (exceptions.Count > 0)
throw new AggregateException(exceptions);
return new Book("Better GraphQL Errors");
}
[Error<InsufficientFundException>]
[Error<OutOfStockException>]
public async Task<Book> BuyBook(string title) {
var exceptions = new List<Exception>();
if (OutOfStock)
exceptions.Add(
new OutOfStockException() {
SuggestedBook = new Book("Error Handling in GraphQL"),
BackInStockDate = DateTimeOffset.UtcNow.AddDays(5),
}
);
if (InsufficientFund)
exceptions.Add(new InsufficientFundException(10, 50));
if (exceptions.Count > 0)
throw new AggregateException(exceptions);
return new Book("Better GraphQL Errors");
}
The mutation returns an errors list that includes the error type and relevant error message. The mutation uses a union type to differentiate between different types of errors and provides specific error fields, such as currentBalance
or backInStockDate
, depending on the error type. We can use ... on Error
to catch any error that implements the Error interface. The __typename
field in the response will indicate the actual type of error that occurred, which can then be handled differently in the client application.
BuyBook Mutation
mutation {
buyBook(input: { title: "Better GraphQL Errors" }) {
book {
title
releaseDate
}
errors {
__typename
... on Error {
message
}
... on InsufficientFundError {
currentBalance
requiredAmount
}
... on OutOfStockError {
backInStockDate
suggestedBook {
title
}
}
}
}
}
mutation {
buyBook(input: { title: "Better GraphQL Errors" }) {
book {
title
releaseDate
}
errors {
__typename
... on Error {
message
}
... on InsufficientFundError {
currentBalance
requiredAmount
}
... on OutOfStockError {
backInStockDate
suggestedBook {
title
}
}
}
}
}
BuyBook Response
{
"data": {
"buyBookOrMultiError": {
"book": null,
"errors": [
{
"__typename": "OutOfStockError",
"message": "Out Of Stock",
"backInStockDate": "2023-03-09T22:43:46.433Z",
"suggestedBook": { "title": "Suggested Book" }
},
{
"__typename": "InsufficientFundError",
"message": "Not Enough Money 💵",
"currentBalance": 10,
"requiredAmount": 50
}
]
}
}
}
{
"data": {
"buyBookOrMultiError": {
"book": null,
"errors": [
{
"__typename": "OutOfStockError",
"message": "Out Of Stock",
"backInStockDate": "2023-03-09T22:43:46.433Z",
"suggestedBook": { "title": "Suggested Book" }
},
{
"__typename": "InsufficientFundError",
"message": "Not Enough Money 💵",
"currentBalance": 10,
"requiredAmount": 50
}
]
}
}
}
Mutation Response Diagram
Conclusion
Error handling is an essential aspect of building robust and reliable GraphQL APIs. Understanding the different types of errors, such as network errors, unintentional error responses, and intentional error responses, is crucial in implementing effective error handling strategies. Additionally, utilizing best practices such error unions, and mutation conventions can simplify error handling and improve the overall user experience. Proper error handling can provide users with clear and informative error messages, allowing them to better understand and address any issues that may arise.
Github Repo
Check out the GitHub repository for the code of the examples provided above for the GraphQL error handling.