Error Handling
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 parametersE4012- Unauthorized - Invalid or missing authenticationE4031- Forbidden - Insufficient permissionsE4041- Not Found - Resource not foundE4091- Conflict - Resource already existsE4221- Unprocessable Entity - Business rule violationE4291- Too Many Requests - Rate limit exceeded
Server Errors (5xx)
E5001- Internal Server Error - Generic errorE5002- Database Error - Database operation failedE5003- External Service Error - External API failedE5031- Service Unavailable - Service temporarily unavailable
Best Practices
DO ✅
- Use consistent error codes
- Return user-friendly messages
- Log detailed error information
- Sanitize error messages (don't expose internals)
- Use appropriate HTTP status codes
- Handle all error cases
DON'T ❌
- Don't expose stack traces to clients
- Don't return database errors directly
- Don't ignore errors
- Don't use generic error messages everywhere
- Don't expose sensitive information in errors
Next Steps
- Learn about authentication
- Implement middleware
- Add audit logging