Skip to main content

Command Palette

Search for a command to run...

GraphQL in .NET: Error Handling

Updated
4 min read
GraphQL in .NET: Error Handling

In our journey with GraphQL, sooner or later, we are going to need to pick a strategy to handle errors during mutations. There is a great post "A Guide to GraphQL Errors" where the author shows several options (its pros and cons). Based on this, we will implement the last stage presented there: Stage 6a: Error Union List + Interface.

We will modify the code presented here, please clone or download it. Open the solution and add a file named IError.cs with the following content:

public interface IError
{
    string Message { get; set; }
}

public class NotEmptyError : IError
{
    public string Message { get; set; } = null!;

    public NotEmptyError(string field)
    {
        Message= $"The {field} is required";
    }
}

public class MaxLengthError : IError
{
    public string Message { get; set; } = null!;

    public MaxLengthError(string field, int maxLength)
    {
        Message = $"The max length for {field} is {maxLength}";
    }
}

Open the PostMutations.cs file, and modify the AddPostPayload record as follow:

public record AddPostPayload(Post? post, IError[]? errors);

In the same file, modify the AddPost method with the following content:

public AddPostPayload AddPost(AddPostInput input, [Service] Storage storage)
{
    var errors = new List<IError>();

    if(string.IsNullOrEmpty(input.Body))
    {
        errors.Add(new NotEmptyError(nameof(AddPostInput.Body)));
    }

    if (string.IsNullOrEmpty(input.Title))
    {
        errors.Add(new NotEmptyError(nameof(AddPostInput.Title)));
    }

    if (input.Body?.Length >= 256)
    {
        errors.Add(new MaxLengthError(nameof(AddPostInput.Body), 256));
    }

    if (input.Title?.Length >= 1024)
    {
        errors.Add(new MaxLengthError(nameof(AddPostInput.Title), 1024));
    }

    if (errors.Any())
    {
        return new AddPostPayload(null, errors.ToArray());
    }

    var post = new Post() { Id = Guid.NewGuid(), Body = input.Body, Title = input.Title };

    storage.Posts.Add(post);

    return new AddPostPayload(post, null);
}

All the code to support the errors is done, time to setup the schema to support it. Start creating a file named ErrorType.cs. Here we are going to register an interface, two errors and an union:

public class ErrorType : InterfaceType<IError>
{
    protected override void Configure(
        IInterfaceTypeDescriptor<IError> descriptor)
    {
    }
}

public class MaxLengthErrorType : ObjectType<MaxLengthError>
{
    protected override void Configure(
        IObjectTypeDescriptor<MaxLengthError> descriptor)
    {
        descriptor.Implements<ErrorType>();
    }
}

public class NotEmptyErrorType : ObjectType<NotEmptyError>
{
    protected override void Configure(
        IObjectTypeDescriptor<NotEmptyError> descriptor)
    {
        descriptor.Implements<ErrorType>();
    }
}

public class AddPostPayloadErrorType : UnionType
{
    protected override void Configure(IUnionTypeDescriptor descriptor)
    {
        descriptor.Name("AddPostPayloadError");
        descriptor.Type<MaxLengthErrorType>();
        descriptor.Type<NotEmptyErrorType>();
    }
}

Create a file named AddPostPayloadType.cs with the following content:

public class AddPostPayloadType : ObjectType<AddPostPayload>
{
    protected override void Configure(IObjectTypeDescriptor<AddPostPayload> descriptor)
    {
        descriptor
            .Field(f => f.post)
            .Type<PostType>();

        descriptor
            .Field(f=>f.errors)
            .Type<ListType<AddPostPayloadErrorType>>();
    }
}

And finally, update the PostMutationsType.cs file as follows:

public class PostMutationsType : ObjectType<PostMutations>
{
    protected override void Configure(
        IObjectTypeDescriptor<PostMutations> descriptor)
    {
        descriptor.Field(f => f.AddPost(default!, default!)).Type<AddPostPayloadType>();
        descriptor.Field(f => f.RemovePost(default!, default!));
        descriptor.Field(f => f.EditPost(default!, default!));
    }
}

Run the solution, go to https://localhost:7121/graphql/ and run the following mutation:

mutation {
  addPost(input: { body: "", title: "" }) {
    post {
      id
    }
    errors {
      ... on MaxLengthError {
        message
      }
      ... on NotEmptyError {
        message
      }
    }
  }
}

To see a result such as:

{
  "data": {
    "addPost": {
      "post": null,
      "errors": [
        {
          "message": "The Body is required"
        },
        {
          "message": "The Title is required"
        }
      ]
    }
  }
}

We did it! but there is a lot of boilerplate code. Luckily, HotChocolate has an implementation of this pattern out of the box with a difference, the errors are going to be thrown as exceptions. Create a new file NotFoundException.cs as follows:

public class NotFoundException : Exception
{
    public NotFoundException(Guid id) : base($"The with {id} was not found")
    {
    }
}

Open the PostMutations.cs file and modify the EditPost method with the following content:

public Post EditPost(EditPostInput input, [Service] Storage storage)
{
    var post = storage.Posts.FirstOrDefault(post => post.Id == input.Id);
    if(post==null)
    {
        throw new NotFoundException(input.Id);
    }
    post.Body = input.Body;
    return post;
}

Go to the PostMutationsType.cs file and update the code as follow:

public class PostMutationsType : ObjectType<PostMutations>
{
    protected override void Configure(
        IObjectTypeDescriptor<PostMutations> descriptor)
    {
        descriptor.Field(f => f.AddPost(default!, default!)).Type<AddPostPayloadType>();
        descriptor.Field(f => f.RemovePost(default!, default!));
        descriptor.Field(f => f.EditPost(default!, default!))
            .UseMutationConvention()
            .Error<NotFoundException>();
    }
}

Finally, go to the Program.cs file and enable the mutation conventions with AddMutationConventions:

builder.Services
    .AddGraphQLServer()
    .AddMutationConventions(applyToAllMutations: false)
    .AddQueryType<PostQueriesType>()
    .AddMutationType<PostMutationsType>()
    .AddFiltering()
    .AddSorting();

Run the solution and run the following mutation:

mutation {
  editPost(input: { body: "abc", id: "5e629e6fb8d64126196c7d12cb6f8a6a" }) {
    post {
      id
    }
    errors {
      ... on NotFoundError {
        message
      }
    }
  }
}

To get the same result as our manual implementation:

{
  "data": {
    "editPost": {
      "post": null,
      "errors": [
        {
          "message": "The with 5e629e6f-b8d6-4126-196c-7d12cb6f8a6a was not found"
        }
      ]
    }
  }
}

You can check more about this feature here. The final code of this post is available here. Thanks, and happy coding.

254 views

More from this blog

raulnq

170 posts

Somebody who likes to code