Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
23 Oct 2025 · 6 min read ·Article 115 / 125
Go

115 Structured Error Handling in gqlgen Mutations

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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:

go
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:

json
 1{
 2  "errors": [
 3    {
 4      "message": "sql: no rows in result set"
 5    }
 6  ],
 7  "data": {
 8    "updateUser": null
 9  }
10}
Danger

Problem:

  1. A flat error with no detail
  2. No code/status
  3. 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:

FieldTypeExampleDescription
codestring“NOT_FOUND”Specific error code
messagestring“User not found”Message for the user
fieldstring“email”Field related to the error (optional)
extensionsmap{“level”: “warning”}Additional metadata

Example structure in a custom error package:

go
 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.

go
 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.

go
 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:

CodeHttp EquivalentHuman MessageTypically Used For
INVALID_ARGUMENT400Field x is invalidInput field validation
NOT_FOUND404Data not foundEntity/data does not exist
UNAUTHORIZED401Login firstUser not yet logged in
FORBIDDEN403Access deniedNo permission
ALREADY_EXISTS409Email already takenUser/email unique constraint
INTERNAL_SERVER_ERROR500Something went wrongUnknown error
TIMEOUT504Request timeoutSlow downstream request
TOO_MANY_REQUESTS429Rate limit exceededRate limiting error
NOT_IMPLEMENTED501Feature not yet implementedFeature not yet working
DEPENDENCY_FAILURE424Dependent service errorDownstream error
… (etc.)
Danger
In a large project, the mapping list can break past 115 error codes (hence the name of the article 😉).

🚦 Error Handling Flow Diagram in Mutations

Let’s illustrate the error flow in the resolver step by step using mermaid:

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:

json
 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!

Related Articles

💬 Comments