errors

package
v0.18.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2025 License: MIT Imports: 5 Imported by: 0

README

Contributions Welcome Release

Errors Package

The errors package provides a centralized error handling library for Go applications. It standardizes error codes, messages, and HTTP status codes across services, making error management consistent and efficient.

Features

  • Standardized Error Codes: Uses a consistent error code format (xyyzzz) across services.
  • Service Prefixes: Allows setting a service-specific prefix for error codes.
  • Domain Errors: Provides a DomainError interface for custom errors.
  • Base Error Embedding: Encourages embedding BaseError for consistency.
  • Utilities: Includes helper functions for wrapping, unwrapping, and extracting errors.
  • Category Validation: Validates that error codes align with predefined categories.

Getting Started

Setting the Service Prefix

Before using the error handling library, set the service-specific prefix. This helps in identifying which service an error originated from.

import "github.com/kittipat1413/go-common/framework/errors"

func init() {
    errors.SetServicePrefix("USER-SVC")
}
Defining Custom Errors

Define custom errors by embedding *errors.BaseError in your error type. This ensures that custom errors conform to the DomainError interface and can be properly handled by the error utilities.

package myerrors

import (
    "fmt"
    "net/http"

    "github.com/kittipat1413/go-common/framework/errors"
)

const (
    // StatusCodeUserNotFound indicates that the requested user could not be found.
    // Error Code Convention:
    // - Main Category: 4 (Client Errors)
    // - Subcategory: 02 (Not Found)
    // - Specific Error Code: 001 (Defined by the service)
    StatusCodeUserNotFound = "402001"
)

type UserNotFoundError struct {
    *errors.BaseError
}

func NewUserNotFoundError(userID string) (*UserNotFoundError, error) {
    baseErr, err := errors.NewBaseError(
        StatusCodeUserNotFound,
        fmt.Sprintf("User with ID %s not found", userID),
        map[string]string{"user_id": userID},
    )
    if err != nil {
        return nil, err
    }
    return &UserNotFoundError{BaseError: baseErr}, nil
}
Simplify Error Constructors

To avoid handling errors every time you create a custom error, you can design your constructors to handle any internal errors themselves. This way, your error creation functions can have a simpler signature, returning only the custom error. You can handle internal errors by:

  • Panicking

    If errors.NewBaseError returns an error, it likely indicates a misconfiguration or coding error (e.g., invalid error code). In such cases, it's acceptable to panic during development to catch the issue early.

    func NewUserNotFoundError(userID string) *UserNotFoundError {
        baseErr, err := errors.NewBaseError(
            StatusCodeUserNotFound,
            fmt.Sprintf("User with ID %s not found", userID),
            map[string]string{"user_id": userID},
        )
        if err != nil {
            panic(fmt.Sprintf("Failed to create BaseError: %v", err))
        }
        return &UserNotFoundError{BaseError: baseErr}
    }
    
  • Returning an Error Interface ✅

    If you want the option to handle the error in the calling function, you can modify your constructor to return an error interface. This allows proper handling of ErrBaseErrorCreationFailed, which is returned when NewBaseError fails due to invalid error codes or categories.

    func NewUserNotFoundError(userID string) error {
        baseErr, err := errors.NewBaseError(
            StatusCodeUserNotFound,
            fmt.Sprintf("User with ID %s not found", userID),
            map[string]string{"user_id": userID},
        )
        if err != nil {
            return err
        }
        return &UserNotFoundError{BaseError: baseErr}
    }
    
  • Using init() to Initialize Predefined Errors

    You can simplify handling predefined errors by initializing them at the package level. This approach removes the need to handle errors every time you use these predefined errors. If NewBaseError fails during initialization (e.g., due to a misconfiguration), log.Fatal will immediately halt the program and output the error. This way, issues are caught early at startup rather than during runtime.

    package myerrors
    
    import (
        "fmt"
        "log"
        "net/http"
        "github.com/kittipat1413/go-common/framework/errors"
    )
    
    // Predefined errors as package-level variables.
    var (
        ErrBadRequest        *BadRequestError
        ErrNotFound          *NotFoundError
    )
    
    // init initializes predefined errors at package load.
    func init() {
        var err error
    
        // Initialize BadRequestError
        ErrBadRequest, err = newBadRequestError()
        if err != nil {
            log.Fatal(fmt.Sprintf("failed to initialize ErrBadRequest: %v", err))
        }
    
        // Initialize NotFoundError
        ErrNotFound, err = newNotFoundError()
        if err != nil {
            log.Fatal(fmt.Sprintf("failed to initialize ErrNotFound: %v", err))
        }
    }
    
    // BadRequestError is a predefined error for bad request cases.
    type BadRequestError struct {
        *BaseError
    }
    
    // Helper function to initialize BadRequestError.
    func newBadRequestError() (*BadRequestError, error) {
        baseErr, err := errors.NewBaseError(
            StatusCodeGenericBadRequestError,
            "", // Empty message to use the default message.
            nil,
        )
        if err != nil {
            return nil, err
        }
        return &BadRequestError{BaseError: baseErr}, nil
    }
    
    // NotFoundError is a predefined error for not found cases.
    type NotFoundError struct {
        *BaseError
    }
    
    // Helper function to initialize NotFoundError.
    func newNotFoundError() (*NotFoundError, error) {
        baseErr, err := errors.NewBaseError(
            StatusCodeGenericNotFoundError,
            "", // Empty message to use the default message.
            nil,
        )
        if err != nil {
            return nil, err
        }
        return &NotFoundError{BaseError: baseErr}, nil
    }
    

    After defining these errors, you can use them directly in your code by referencing myerrors.ErrBadRequest or myerrors.ErrNotFound. Since they are pre-initialized at the package level, they are always available without needing additional error handling for creation.

Why Wrap BaseError in a Custom Type?

In Go, it’s common to wrap a base error type inside a more specific domain error type (like UserNotFoundError). Here’s why this approach is beneficial:

  • Stronger Type Assertions
    • When handling errors, using a custom error type allows for better type checking with errors.As().
      err := NewUserNotFoundError("12345")
    
      var userErr *UserNotFoundError
      if errors.As(err, &userErr) {
          fmt.Println("Handling UserNotFoundError:", userErr.GetMessage())
      }
    
  • Better Encapsulation of Business Logic
    • A custom error type keeps domain logic inside the error itself, making it easier to manage.
    type UserNotFoundError struct {
        *errors.BaseError
    }
    
    // Example: Define custom logic for this error
    func (e *UserNotFoundError) IsCritical() bool {
        return false // A missing user is not considered a critical failure
    }
    
    // Now, error handling can adapt based on business logic:
    var userErr *UserNotFoundError
    if errors.As(err, &userErr) && userErr.IsCritical() {
        // Handle it differently if it's a critical issue
    }
    
  • Improves Readability & Maintains Domain Clarity
    • Without a Custom Error Type:
    return errors.NewBaseError(StatusCodeUserNotFound, "User not found", nil)
    
    • With a Custom Error Type:
    return NewUserNotFoundError(userID)
    
Using the Error Handling Utilities

Adding Context with a Prefix: Use errors.WrapErrorWithPrefix to add context to an error with a specified prefix. This helps in tracking where the error occurred. If the error is nil, it does nothing.

func someFunction() (err error) {
    defer errors.WrapErrorWithPrefix("[someFunction]", &err)
    // Function logic...
    return
}

Wrapping Errors: Use errors.WrapError to combine multiple errors into one. If either error is nil, it returns the non-nil error. If both are non-nil, it wraps the new error around the original error.

user, err := getUser()
if err != nil {
    // Creating a domain-specific error
    domainErr := errors.New("user not found")

    // Wrapping the domain error around the original error
    return errors.WrapError(err, domainErr)
}

Unwrapping Domain Errors: Use errors.UnwrapDomainError to extract the DomainError from an error chain, allowing for specialized handling of domain-specific errors.

func handleError(err error) {
    if domainErr := errors.UnwrapDomainError(err); domainErr != nil {
        // Handle domain error
    } else {
        // Handle generic error
    }
}

Error Code Convention

Error codes follow the xyyzzz format:

  • x: Main category (e.g., 4 for Client Errors).
  • yy: Subcategory (e.g., 01 for Bad Request Errors).
  • zzz: Specific error code (e.g., 001 for a particular invalid parameter).

Example: 401001 could represent an invalid username parameter.

Error Categories

Defined categories and their descriptions:

  • 200zzz: Success
    • 201zzz: Partial Success
    • 202zzz: Accepted
  • 400zzz: Client Errors
    • 401zzz: Bad Request
    • 402zzz: Not Found
    • 403zzz: Conflict
    • 404zzz: Unprocessable Entity
  • 500zzz: Server Errors
    • 501zzz: Database Errors
    • 502zzz: 3rd Party Errors
    • 503zzz: Service Unavailable
  • 900zzz: Security Errors
    • 901zzz: Unauthorized
    • 902zzz: Forbidden

The validCategories map in categories.go maintains the valid categories and their descriptions.

Examples

You can find a complete working example in the repository under framework/errors/example.

Best Practices

  • Consistency: Always define error codes and messages in a centralized place to maintain consistency.
  • Embedding BaseError: Ensure all custom errors embed *errors.BaseError to integrate with the error handling utilities.
  • Category Alignment: When defining new error codes, make sure they align with the predefined categories and use the correct HTTP status codes.
  • Avoid Manual Synchronization: Use the centralized data structures for categories and error codes to prevent inconsistencies.

Documentation

Index

Constants

View Source
const (
	// Success (2yyzzz)
	StatusCodeSuccess        = "200000" // General Success
	StatusCodePartialSuccess = "201000" // Partial Success (e.g., batch processing)
	StatusCodeAccepted       = "202000" // Accepted (e.g., long-running task queued)

	// Client Errors (4yyzzz)
	StatusCodeGenericClientError              = "400000" // General Client Error
	StatusCodeGenericBadRequestError          = "401000" // Bad Request (e.g., missing or invalid parameters)
	StatusCodeGenericNotFoundError            = "402000" // Not Found (e.g., resource not found)
	StatusCodeGenericConflictError            = "403000" // Conflict (e.g., resource already exists)
	StatusCodeGenericUnprocessableEntityError = "404000" // Unprocessable Entity (e.g., validation error)

	// Server Errors (5yyzzz)
	StatusCodeGenericInternalServerError     = "500000" // General Internal Server Error
	StatusCodeGenericDatabaseError           = "501000" // Database Error
	StatusCodeGenericThirdPartyError         = "502000" // Third-party Error
	StatusCodeGenericServiceUnavailableError = "503000" // Service Unavailable (e.g., maintenance mode)

	// Authentication and Authorization Errors (9yyzzz)
	StatusCodeGenericAuthError         = "900000" // General Authentication Error
	StatusCodeGenericUnauthorizedError = "901000" // Unauthorized (e.g., missing or invalid token)
	StatusCodeGenericForbiddenError    = "902000" // Forbidden (e.g., insufficient permissions)
)

Error code constants following the 'xyyzzz' ([x][yy][zzz]) convention.

  • 'x' - Main category,
  • 'yy' - Subcategory,
  • 'zzz' - Specific error code within the subcategory.
View Source
const DefaultServicePrefix = "ERR" // DefaultServicePrefix is the default prefix used for errors.

Variables

View Source
var ErrBaseErrorCreationFailed = errors.New("BaseError creation failed")

Functions

func GetCategoryDescription

func GetCategoryDescription(xyy string) string

GetCategoryDescription returns the description of the 'xyy' category. If the category does not exist, it returns "Unknown Category".

func GetCategoryHTTPStatus

func GetCategoryHTTPStatus(xyy string) int

GetCategoryHTTPStatus returns the HTTP status code of the 'xyy' category. If the category does not exist, it returns 500.

func GetFullCode

func GetFullCode(code string) string

GetFullCode constructs the full error code with the service prefix. If servicePrefix is "SVC" and code is "200000", it returns "SVC-200000".

func GetServicePrefix

func GetServicePrefix() string

GetServicePrefix returns the current service prefix.

func IsValidCategory

func IsValidCategory(xyy string) bool

IsValidCategory validates the 'xyy' part of an error code. It returns true if the category exists, and false otherwise.

func NewAuthenticationError

func NewAuthenticationError(message string, data interface{}) error

NewAuthenticationError creates a new AuthenticationError instance using the generic authentication error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewBadRequestError

func NewBadRequestError(message string, data interface{}) error

NewBadRequestError creates a new BadRequestError instance using the generic bad request error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewClientError

func NewClientError(message string, data interface{}) error

NewClientError creates a new ClientError instance using the generic client error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewConflictError

func NewConflictError(message string, data interface{}) error

NewConflictError creates a new ConflictError instance using the generic conflict error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewDatabaseError

func NewDatabaseError(message string, data interface{}) error

NewDatabaseError creates a new DatabaseError instance using the generic database error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewForbiddenError

func NewForbiddenError(message string, data interface{}) error

NewForbiddenError creates a new ForbiddenError instance using the generic forbidden error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewInternalServerError

func NewInternalServerError(message string, data interface{}) error

NewInternalServerError creates a new InternalServerError instance using the generic internal error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewNotFoundError

func NewNotFoundError(message string, data interface{}) error

NewNotFoundError creates a new NotFoundError instance using the generic not found error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewServiceUnavailableError added in v0.11.2

func NewServiceUnavailableError(message string, data interface{}) error

NewServiceUnavailableError creates a new ServiceUnavailableError instance using the generic service unavailable error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewThirdPartyError

func NewThirdPartyError(message string, data interface{}) error

NewThirdPartyError creates a new ThirdPartyError instance using the generic third-party error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewUnauthorizedError

func NewUnauthorizedError(message string, data interface{}) error

NewUnauthorizedError creates a new UnauthorizedError instance using the generic unauthorized error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func NewUnprocessableEntityError

func NewUnprocessableEntityError(message string, data interface{}) error

NewUnprocessableEntityError creates a new UnprocessableEntityError instance using the generic unprocessable entity error code. If the `message` parameter is an empty string (""), the default message for the error code will be used.

func SetServicePrefix

func SetServicePrefix(prefix string)

SetServicePrefix sets the service-specific prefix (e.g., "USER-SVC"). It converts the prefix to uppercase to maintain consistency. If an empty prefix is provided, the default prefix (ERR) is used.

func WrapError

func WrapError(original, new error) error

WrapError wraps two errors into one. If either error is nil, it returns the non-nil error. If both are non-nil, it wraps the new error around the original error.

func WrapErrorWithPrefix

func WrapErrorWithPrefix(prefix string, errptr *error)

WrapErrorWithPrefix wraps the input error with a prefix. If the error is nil, it does nothing.

Types

type AuthenticationError

type AuthenticationError struct {
	*BaseError
}

func (*AuthenticationError) As added in v0.18.0

func (e *AuthenticationError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type BadRequestError

type BadRequestError struct {
	*BaseError
}

func (*BadRequestError) As added in v0.18.0

func (e *BadRequestError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type BaseError

type BaseError struct {
	// contains filtered or unexported fields
}

BaseError provides a default implementation of the DomainError interface. It can be embedded in other error types to avoid code duplication.

func ExtractBaseError

func ExtractBaseError(err error) *BaseError

ExtractBaseError attempts to extract the BaseError from the error's concrete type. It supports both pointer and non-pointer types, checking if the error directly embeds a *BaseError (one layer deep).

func NewBaseError

func NewBaseError(code, message string, data interface{}) (*BaseError, error)

NewBaseError creates a new BaseError instance. If the message is empty, it uses the default message from `getDefaultMessages()` based on the error code.

The error code should follow the 'xyyzzz' convention:

  • 'x' (first digit): main error category.
  • 'yy' (second digit): subcategory.
  • 'zzz' (last three digits): specific error detail.

**Note:** The 'xyy' prefix of the code must match a valid category defined in `validCategories`.

func (*BaseError) Code

func (e *BaseError) Code() string

func (*BaseError) Error

func (e *BaseError) Error() string

func (*BaseError) GetData

func (e *BaseError) GetData() interface{}

func (*BaseError) GetHTTPCode

func (e *BaseError) GetHTTPCode() int

func (*BaseError) GetMessage

func (e *BaseError) GetMessage() string

type ClientError

type ClientError struct {
	*BaseError
}

func (*ClientError) As added in v0.18.0

func (e *ClientError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type ConflictError

type ConflictError struct {
	*BaseError
}

func (*ConflictError) As added in v0.18.0

func (e *ConflictError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type DatabaseError

type DatabaseError struct {
	*BaseError
}

func (*DatabaseError) As added in v0.18.0

func (e *DatabaseError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type DomainError

type DomainError interface {
	// GetHTTPCode returns the HTTP status code associated with the error.
	GetHTTPCode() int

	// Code returns the full error code, including the service prefix (e.g., 'SVC-401000').
	Code() string

	// GetMessage returns the error message.
	GetMessage() string

	// GetData returns any additional data associated with the error.
	GetData() interface{}

	// Error implements the standard error interface.
	Error() string
}

DomainError is the interface that all custom errors in the framework implement. It provides methods to retrieve the error code, message, HTTP status code and additional data.

Error Code Convention: The error code follows the format 'xyyzzz' ([x][yy][zzz])

  • 'x' (first digit) represents the main error category (e.g., '4' for Client Error).
  • 'yy' (second digit) represents the subcategory (e.g., '01' for Bad Request).
  • 'zzz' (last three digits) provide specific details about the error.

This convention helps in categorizing errors consistently across services.

func UnwrapDomainError

func UnwrapDomainError(err error) DomainError

UnwrapDomainError attempts to find a DomainError in the error chain. The error should implement the DomainError interface and have a BaseError embedded. It unwraps the error chain and checks each error to see if it is a DomainError and if it contains a BaseError. If such an error is found, it is returned.

type ForbiddenError

type ForbiddenError struct {
	*BaseError
}

func (*ForbiddenError) As added in v0.18.0

func (e *ForbiddenError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type InternalServerError

type InternalServerError struct {
	*BaseError
}

func (*InternalServerError) As added in v0.18.0

func (e *InternalServerError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type NotFoundError

type NotFoundError struct {
	*BaseError
}

func (*NotFoundError) As added in v0.18.0

func (e *NotFoundError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type ServiceUnavailableError added in v0.11.2

type ServiceUnavailableError struct {
	*BaseError
}

func (*ServiceUnavailableError) As added in v0.18.0

func (e *ServiceUnavailableError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type ThirdPartyError

type ThirdPartyError struct {
	*BaseError
}

func (*ThirdPartyError) As added in v0.18.0

func (e *ThirdPartyError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type UnauthorizedError

type UnauthorizedError struct {
	*BaseError
}

func (*UnauthorizedError) As added in v0.18.0

func (e *UnauthorizedError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

type UnprocessableEntityError

type UnprocessableEntityError struct {
	*BaseError
}

func (*UnprocessableEntityError) As added in v0.18.0

func (e *UnprocessableEntityError) As(target interface{}) bool

As checks if the error can be assigned to the target interface. It supports both pointer and non-pointer types for the target.

Directories

Path Synopsis
example
gin command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL