Backend Development 22 min read

Standardizing RESTful API Error Handling with Business Error Codes and an Error Package in Go

This article explains how to design a unified business error‑code system for RESTful APIs, demonstrates the structure and format of such codes, provides a Go implementation with structs, interfaces and formatting methods, and shows practical usage in a Gin web server including logging and notification strategies.

Go Programming World
Go Programming World
Go Programming World
Standardizing RESTful API Error Handling with Business Error Codes and an Error Package in Go

Modern Web APIs usually follow the RESTful style and return an HTTP status code together with a JSON body containing a Code (error number) and a Message (error description). While HTTP status codes indicate success or failure, they cannot express detailed business errors, so a separate, unique business error code is needed.

Why a business error code? A 500 error may be caused by a database timeout or a logic bug, and developers cannot pinpoint the problem from the HTTP status alone. A dedicated error code, readable and tied to a specific component, makes debugging and monitoring much easier, especially in micro‑service architectures.

Design goals for the error code :

Uniqueness – each code must be globally unique.

Readability – the code should reveal the subsystem and error type.

Mapping to HTTP status – the code should allow quick derivation of the appropriate HTTP status.

Typical designs from public APIs (Alibaba Cloud, Sina) are examined. Alibaba returns a rich JSON object with Code , Message , RequestId , HostId and Recommend . Sina uses a compact numeric string such as 20502 , which is easier for programs to handle.

Proposed business error‑code format :

40001002
│ │ │
│ │ └─ component‑internal error (3 digits, up to 1000 values)
│ └─ component identifier (2 digits, up to 99 components)
└─ HTTP status code (first 3 digits)

The code is an 8‑digit integer (type int ) so arithmetic can extract the HTTP status (e.g., code / 1000 ).

Standard API error response format :

{
  "code": 50000000,
  "message": "系统错误",
  "reference": "https://github.com/jianghushinian/gokit/tree/main/errors"
}

Only code , message and optional reference are exposed to clients; internal fields such as cause and stack are omitted.

Go implementation :

type apiCode struct {
    code int
    msg  string
    ref  string
}

func NewAPICode(code int, message string, reference ...string) APICoder {
    ref := ""
    if len(reference) > 0 {
        ref = reference[0]
    }
    return &apiCode{code: code, msg: message, ref: ref}
}

The APICoder interface defines Code() , Message() , Reference() and HTTPStatus() . An apiError struct wraps an APICoder , an optional cause, and a stack trace, implementing Error() , Unwrap() , Format() , MarshalJSON() and UnmarshalJSON() so that errors can be logged, formatted, and serialized.

Usage with Gin :

package main

import (
    "errors"
    "fmt"
    "strconv"
    "github.com/gin-gonic/gin"
    apierr "github.com/jianghushinian/gokit/errors"
)

var (
    ErrAccountNotFound = errors.New("account not found")
    ErrDatabase        = errors.New("database error")
)

    CodeBadRequest   = NewAPICode(40001001, "请求不合法")
    CodeNotFound    = NewAPICode(40401001, "资源未找到")
    CodeUnknownError = NewAPICode(50001001, "系统错误", "https://github.com/jianghushinian/gokit/tree/main/errors")
)

type Account struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func ShowAccount(c *gin.Context) {
    idStr := c.Param("id")
    aid, err := strconv.Atoi(idStr)
    if err != nil {
        ResponseError(c, apierr.WrapC(CodeBadRequest, err))
        return
    }
    account, err := AccountOne(aid)
    if err != nil {
        switch {
        case errors.Is(err, ErrAccountNotFound):
            err = apierr.NewAPIError(CodeNotFound, err)
        case errors.Is(err, ErrDatabase):
            err = apierr.NewAPIError(CodeUnknownError, fmt.Errorf("account %d: %w", aid, err))
        }
        ResponseError(c, err)
        return
    }
    ResponseOK(c, account)
}

Helper functions ResponseOK and ResponseError send JSON responses, log the full error (including stack) and, for server‑side errors (≥500), simulate sending a structured JSON email.

Best‑practice suggestions :

Limit the number of HTTP status codes used (400, 401, 403, 404, 500).

Wrap low‑level errors with apierr.WrapC at the handler layer.

Use middleware to log errors, send alerts, and return only safe fields to the client.

In summary, the article provides a complete design of a business error‑code scheme, a Go error‑package implementation, and practical guidance for integrating it into a Gin‑based service, improving both developer experience and operational observability.

backendGosoftware designerror handlingRESTful APIError CodesGin
Go Programming World
Written by

Go Programming World

Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.