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​