115 Structured Error Handling in gqlgen Mutations
115 Structured Error Handling in gqlgen Mutations
In modern application development, GraphQL has become one of the most important backbones for communication between client and server. One of the popular frameworks for implementing GraphQL in Golang is gqlgen. However, developers often overlook error handling, or let errors leak out without any clear pattern, especially in the realm of mutations. In this article, I will take an in-depth look at: “115 Structured Error Handling in gqlgen Mutations”.
Why did I name it 115 error handling? Because in reality, the errors that show up can generally be divided into 3 layers (1xx: validation, 1xx: authentication/authorization, 5xx: business/internal error), and the total number of error-handling best practices in a production system can run into the hundreds of patterns. Now, let’s break down and implement structured error handling in gqlgen mutations.
🔥 Flat Error Handling vs Structured Error Handling
Many GraphQL codebases only do the following in their mutations:
1func (r *mutationResolver) UpdateUser(ctx context.Context, input UpdateUserInput) (*User, error) {
2 user, err := updateUserInDB(input)
3 if err != nil {
4 return nil, err // The error is unclear!
5 }
6 return user, nil
7}The result looks like this when consumed by the client:
1{
2 "errors": [
3 {
4 "message": "sql: no rows in result set"
5 }
6 ],
7 "data": {
8 "updateUser": null
9 }
10}Problem:
- A flat error with no detail
- No code/status
- No guidance for the client
🧑💻 Best Practice: Structured Error Handling in gqlgen
1. Defining the Error Response Format
The first step is to agree on a single standard error object format. Usually, the optimal format for the client looks like this:
| Field | Type | Example | Description |
|---|---|---|---|
| code | string | “NOT_FOUND” | Specific error code |
| message | string | “User not found” | Message for the user |
| field | string | “email” | Field related to the error (optional) |
| extensions | map | {“level”: “warning”} | Additional metadata |
Example structure in a custom error package:
1package errors
2
3type GraphQLError struct {
4 Code string `json:"code"`
5 Message string `json:"message"`
6 Field string `json:"field,omitempty"`
7 Extensions map[string]interface{} `json:"extensions,omitempty"`
8}
9
10func NewGraphQLError(code, message string, field ...string) *GraphQLError {
11 err := &GraphQLError{
12 Code: code,
13 Message: message,
14 }
15 if len(field) > 0 {
16 err.Field = field[0]
17 }
18 return err
19}2. Creating a Helper to Convert Go Errors into GraphQL Errors
We need to adapt Go errors so they can be presented in the GraphQL error extensions format.
1import "github.com/vektah/gqlparser/v2/gqlerror"
2
3// Convert a custom error into a gqlerror
4func ToGQLError(e *GraphQLError) *gqlerror.Error {
5 gqlErr := &gqlerror.Error{
6 Message: e.Message,
7 Extensions: map[string]interface{}{
8 "code": e.Code,
9 },
10 }
11 if e.Field != "" {
12 gqlErr.Extensions["field"] = e.Field
13 }
14 for k, v := range e.Extensions {
15 gqlErr.Extensions[k] = v
16 }
17 return gqlErr
18}💡 Simulation and Implementation in the Mutation Resolver
Let’s simulate a mutation resolver in gqlgen.
1func (r *mutationResolver) UpdateUser(ctx context.Context, input UpdateUserInput) (*User, error) {
2 if input.Email == "" {
3 gErr := errors.NewGraphQLError("INVALID_ARGUMENT", "Email is required", "email")
4 return nil, errors.ToGQLError(gErr)
5 }
6
7 user, err := repo.FindUserByID(input.ID)
8 if err == sql.ErrNoRows {
9 gErr := errors.NewGraphQLError("NOT_FOUND", "User not found")
10 return nil, errors.ToGQLError(gErr)
11 } else if err != nil {
12 gErr := errors.NewGraphQLError("INTERNAL_SERVER_ERROR", "Internal error", "id")
13 return nil, errors.ToGQLError(gErr)
14 }
15
16 // Update user logic...
17 user.Email = input.Email
18 err = repo.SaveUser(user)
19 if err != nil {
20 gErr := errors.NewGraphQLError("INTERNAL_SERVER_ERROR", "Failed updating user")
21 return nil, errors.ToGQLError(gErr)
22 }
23 return user, nil
24}⚡️ Common Error Code Variations: 115 Error Scenarios
For production scale, we need to map at least >100 error codes for various kinds of errors. Below is an example subset of 10 from an error code mapping table:
| Code | Http Equivalent | Human Message | Typically Used For |
|---|---|---|---|
| INVALID_ARGUMENT | 400 | Field x is invalid | Input field validation |
| NOT_FOUND | 404 | Data not found | Entity/data does not exist |
| UNAUTHORIZED | 401 | Login first | User not yet logged in |
| FORBIDDEN | 403 | Access denied | No permission |
| ALREADY_EXISTS | 409 | Email already taken | User/email unique constraint |
| INTERNAL_SERVER_ERROR | 500 | Something went wrong | Unknown error |
| TIMEOUT | 504 | Request timeout | Slow downstream request |
| TOO_MANY_REQUESTS | 429 | Rate limit exceeded | Rate limiting error |
| NOT_IMPLEMENTED | 501 | Feature not yet implemented | Feature not yet working |
| DEPENDENCY_FAILURE | 424 | Dependent service error | Downstream error |
| … (etc.) |
🚦 Error Handling Flow Diagram in Mutations
Let’s illustrate the error flow in the resolver step by step using mermaid:
flowchart TD
A[Client Send Mutation] --> B{Input Validation}
B -- Invalid --> E[[Return INVALID_ARGUMENT]]
B -- Valid --> C[Business Logic & Data Access]
C -- Not Found --> F[[Return NOT_FOUND]]
C -- Constraint error --> G[[Return ALREADY_EXISTS]]
C -- Unexpected error --> H[[Return INTERNAL_SERVER_ERROR]]
C -- Success --> I[Return Result]
📡 Output on the Client Side
With this approach, the error the client receives is much richer:
1{
2 "errors": [
3 {
4 "message": "User not found",
5 "extensions": {
6 "code": "NOT_FOUND"
7 }
8 }
9 ],
10 "data": {
11 "updateUser": null
12 }
13}The client application (for example: React/Flutter) can now:
- Map error codes to show a toast
- Highlight the field that errored
- Distinguish operational errors from exceptions
🚀 Conclusion
Structured error handling in gqlgen mutations is not just about “returning err”; it changes how the backend and frontend teams work together.
Best Practice Checklist
- Define the error object format (code, message, field, extensions)
- Implement error mapping at every point of potential failure
- Use a helper to standardize gqlgen error output
- Maintain a list of error codes (ideally 100++ handlers)
- Make sure the client is aware of and ready to handle error codes
Error handling is not just a technical concern; it is the foundation of communication between backend and frontend. With structured error handling, integration between engineers becomes far more productive and the user experience becomes much better.
References
Want to implement structured error handling in your real-world GraphQL?
Don’t hesitate 🌟 to discuss in the comments or reach me via LinkedIn
— who knows, your lambda error handling might level up into something more structured!