Introduction
Go’s built-in error interface only carries a string message. For production applications, you often need structured errors that carry additional context โ HTTP status codes, field names, error codes, or nested causes. This guide shows how to build custom error types that integrate cleanly with Go’s error handling ecosystem.
The Error Interface
Any type implementing Error() string satisfies the error interface:
type error interface {
Error() string
}
Basic Custom Error
package main
import (
"errors"
"fmt"
)
// Custom error with status code
type AppError struct {
Code int
Message string
Err error // wrapped underlying error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Implement Unwrap so errors.Is/As work through the chain
func (e *AppError) Unwrap() error {
return e.Err
}
func main() {
err := &AppError{
Code: 404,
Message: "user not found",
Err: errors.New("no rows in result set"),
}
fmt.Println(err)
// => [404] user not found: no rows in result set
// Type assertion to access fields
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("Status code: %d\n", appErr.Code)
}
}
HTTP Error Type
A common pattern for web applications:
package apierr
import "fmt"
type Error struct {
StatusCode int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
err error
}
func (e *Error) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *Error) Unwrap() error { return e.err }
// Constructor functions for common errors
func NotFound(resource string) *Error {
return &Error{
StatusCode: 404,
Code: "NOT_FOUND",
Message: resource + " not found",
}
}
func Unauthorized(msg string) *Error {
return &Error{
StatusCode: 401,
Code: "UNAUTHORIZED",
Message: msg,
}
}
func BadRequest(msg string) *Error {
return &Error{
StatusCode: 400,
Code: "BAD_REQUEST",
Message: msg,
}
}
func Internal(err error) *Error {
return &Error{
StatusCode: 500,
Code: "INTERNAL_ERROR",
Message: "an internal error occurred",
err: err,
}
}
func Wrap(err error, statusCode int, code, message string) *Error {
return &Error{
StatusCode: statusCode,
Code: code,
Message: message,
err: err,
}
}
// Usage in handlers
func getUser(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
return apierr.BadRequest("invalid user ID")
}
user, err := db.GetUser(id)
if errors.Is(err, sql.ErrNoRows) {
return apierr.NotFound("user")
}
if err != nil {
return apierr.Internal(err)
}
return json.NewEncoder(w).Encode(user)
}
// Middleware that handles errors
func errorHandler(h func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := h(w, r)
if err == nil {
return
}
w.Header().Set("Content-Type", "application/json")
var apiErr *apierr.Error
if errors.As(err, &apiErr) {
w.WriteHeader(apiErr.StatusCode)
json.NewEncoder(w).Encode(apiErr)
} else {
log.Printf("unhandled error: %v", err)
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{
"code": "INTERNAL_ERROR",
"message": "an internal error occurred",
})
}
}
}
Validation Error Type
type FieldError struct {
Field string
Message string
}
func (e *FieldError) Error() string {
return fmt.Sprintf("field %q: %s", e.Field, e.Message)
}
type ValidationErrors []*FieldError
func (e ValidationErrors) Error() string {
msgs := make([]string, len(e))
for i, fe := range e {
msgs[i] = fe.Error()
}
return strings.Join(msgs, "; ")
}
func (e ValidationErrors) HasErrors() bool {
return len(e) > 0
}
// Usage
func validateUser(u User) error {
var errs ValidationErrors
if u.Name == "" {
errs = append(errs, &FieldError{Field: "name", Message: "is required"})
}
if u.Email == "" {
errs = append(errs, &FieldError{Field: "email", Message: "is required"})
} else if !strings.Contains(u.Email, "@") {
errs = append(errs, &FieldError{Field: "email", Message: "is invalid"})
}
if u.Age < 0 || u.Age > 150 {
errs = append(errs, &FieldError{Field: "age", Message: "must be between 0 and 150"})
}
if errs.HasErrors() {
return errs
}
return nil
}
// Caller
err := validateUser(user)
var valErrs ValidationErrors
if errors.As(err, &valErrs) {
for _, fe := range valErrs {
fmt.Printf(" %s: %s\n", fe.Field, fe.Message)
}
}
Database Error Type
type DBError struct {
Op string // operation: "query", "insert", "update"
Table string
Err error
}
func (e *DBError) Error() string {
return fmt.Sprintf("db %s on %s: %v", e.Op, e.Table, e.Err)
}
func (e *DBError) Unwrap() error { return e.Err }
// Sentinel errors
var (
ErrNotFound = errors.New("record not found")
ErrDuplicate = errors.New("duplicate key")
)
func (s *Store) GetUser(id int) (*User, error) {
var user User
err := s.db.QueryRow("SELECT * FROM users WHERE id = $1", id).Scan(&user)
if err == sql.ErrNoRows {
return nil, &DBError{Op: "query", Table: "users", Err: ErrNotFound}
}
if err != nil {
return nil, &DBError{Op: "query", Table: "users", Err: err}
}
return &user, nil
}
// Caller can check for specific conditions
user, err := store.GetUser(42)
if errors.Is(err, ErrNotFound) {
// handle not found
}
var dbErr *DBError
if errors.As(err, &dbErr) {
log.Printf("database error on table %s: %v", dbErr.Table, dbErr.Err)
}
The String() vs Error() Distinction
type Status struct {
Code int
Err error
}
// String() is for fmt.Println, %v, %s โ human-readable display
func (s *Status) String() string {
return fmt.Sprintf("Status(%d)", s.Code)
}
// Error() satisfies the error interface โ used in error handling
func (s *Status) Error() string {
return fmt.Sprintf("status %d: %v", s.Code, s.Err)
}
s := &Status{Code: 10, Err: errors.New("abc error")}
fmt.Println(s) // calls String(): "Status(10)"
fmt.Println(s.Error()) // calls Error(): "status 10: abc error"
// As an error:
var err error = s
fmt.Println(err) // calls Error(): "status 10: abc error"
Error Codes Pattern (gRPC-style)
type Code int
const (
CodeOK Code = 0
CodeNotFound Code = 1
CodeUnauthorized Code = 2
CodeBadRequest Code = 3
CodeInternal Code = 4
)
func (c Code) String() string {
switch c {
case CodeOK: return "OK"
case CodeNotFound: return "NOT_FOUND"
case CodeUnauthorized: return "UNAUTHORIZED"
case CodeBadRequest: return "BAD_REQUEST"
case CodeInternal: return "INTERNAL"
default: return fmt.Sprintf("Code(%d)", int(c))
}
}
type StatusError struct {
Code Code
Message string
Details map[string]any
}
func (e *StatusError) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// Helper constructors
func NotFound(msg string) *StatusError {
return &StatusError{Code: CodeNotFound, Message: msg}
}
func Internal(msg string, details map[string]any) *StatusError {
return &StatusError{Code: CodeInternal, Message: msg, Details: details}
}
Resources
- Go Blog: Error Handling and Go
- Go Blog: Working with Errors in Go 1.13
- Go errors package
- Effective Go: Errors
Comments