Skip to main content

Error Handling

Developer Intermediate

Proper error handling is crucial for building robust APIs. Learn the conventions and best practices.

Error Code Format

Format: E + HTTP status code + sequence number

Examples:

  • E4001 - Bad Request (400)
  • E4012 - Unauthorized (401)
  • E4031 - Forbidden (403)
  • E4041 - Not Found (404)
  • E5001 - Internal Server Error (500)

Error Response Format

{
"code": "E4001",
"err": "Error message in English"
}

Creating Errors

Simple Error Message

util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Invalid request"))

Error with Details

util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Validation failed", validationErr))

Wrapping Errors

util.RespondWithError(ctx, util.NewError("E5001", err))

Controller Error Handling

func (c *ProductController) GetProduct(ctx *gin.Context) {
id := ctx.Param("id")

// Validate input
if id == "" {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Product ID is required"))
return
}

// Call service
product, err := c.svc.Product().GetByID(ctx.Request.Context(), id)
if err != nil {
// Handle specific errors
if errors.Is(err, service.ErrProductNotFound) {
util.RespondWithError(ctx, util.NewErrorMessage("E4041", "Product not found"))
return
}

// Generic error
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to get product", err))
return
}

util.RespondWithSuccess(ctx, http.StatusOK, product)
}

Service Layer Errors

Define Custom Errors

// service/errors.go
package service

import "errors"

var (
ErrNotFound = errors.New("resource not found")
ErrAlreadyExists = errors.New("resource already exists")
ErrInvalidInput = errors.New("invalid input")
ErrPermissionDenied = errors.New("permission denied")
ErrInsufficientStock = errors.New("insufficient stock")
)

Return Custom Errors

func (s *ProductService) GetByID(ctx context.Context, id string) (*model.Product, error) {
var product model.Product

err := s.db.Where("resource_id = ?", id).First(&product).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}

if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}

return &product, nil
}

Error Mapping

Map Service Errors to HTTP Errors

func mapServiceError(err error) *util.ErrorMessage {
switch {
case errors.Is(err, service.ErrNotFound):
return util.NewErrorMessage("E4041", "Resource not found")
case errors.Is(err, service.ErrAlreadyExists):
return util.NewErrorMessage("E4091", "Resource already exists")
case errors.Is(err, service.ErrInvalidInput):
return util.NewErrorMessage("E4001", "Invalid input")
case errors.Is(err, service.ErrPermissionDenied):
return util.NewErrorMessage("E4031", "Permission denied")
default:
return util.NewErrorMessage("E5001", "Internal server error")
}
}

// Use in controller
func (c *ProductController) GetProduct(ctx *gin.Context) {
product, err := c.svc.Product().GetByID(ctx.Request.Context(), id)
if err != nil {
util.RespondWithError(ctx, mapServiceError(err))
return
}
// ...
}

Validation Errors

func (c *ProductController) CreateProduct(ctx *gin.Context) {
var req CreateProductRequest

if err := ctx.ShouldBindJSON(&req); err != nil {
// Return validation error details
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Validation failed", err))
return
}

// Additional validation
if req.Price < 0 {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Price must be positive"))
return
}

// Continue...
}

Logging Errors

func (s *ProductService) Create(ctx context.Context, req CreateProductRequest) (*model.Product, error) {
logger := log.GetContextLogger(ctx)

product := &model.Product{
Name: req.Name,
Price: req.Price,
}

if err := s.db.Create(product).Error; err != nil {
// Log error with context
logger.Log(
"msg", "failed to create product",
"err", err,
"name", req.Name,
)
return nil, err
}

return product, nil
}

Error Recovery

Middleware automatically recovers from panics:

router.Use(middleware.Recovery())

Common Error Codes

Client Errors (4xx)

  • E4001 - Bad Request - Invalid parameters
  • E4012 - Unauthorized - Invalid or missing authentication
  • E4031 - Forbidden - Insufficient permissions
  • E4041 - Not Found - Resource not found
  • E4091 - Conflict - Resource already exists
  • E4221 - Unprocessable Entity - Business rule violation
  • E4291 - Too Many Requests - Rate limit exceeded

Server Errors (5xx)

  • E5001 - Internal Server Error - Generic error
  • E5002 - Database Error - Database operation failed
  • E5003 - External Service Error - External API failed
  • E5031 - Service Unavailable - Service temporarily unavailable

Best Practices

DO ✅

  1. Use consistent error codes
  2. Return user-friendly messages
  3. Log detailed error information
  4. Sanitize error messages (don't expose internals)
  5. Use appropriate HTTP status codes
  6. Handle all error cases

DON'T ❌

  1. Don't expose stack traces to clients
  2. Don't return database errors directly
  3. Don't ignore errors
  4. Don't use generic error messages everywhere
  5. Don't expose sensitive information in errors

Next Steps