-
Couldn't load subscription status.
- Fork 16
Open
Labels
enhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomershacktoberfesthelp wantedExtra attention is neededExtra attention is needed
Description
🎯 Goal
Implement a structured error handling package that provides consistent, machine-readable error responses across the entire API.
📋 Description
Currently, error responses are handled ad-hoc in handlers with inconsistent formats. A dedicated error package will provide:
- Consistent error response structure
- Error codes for client-side handling
- Better debugging with error details
- Localization support (future)
- Error wrapping and context
✅ Acceptance Criteria
1. Create Error Package Structure
internal/
└── errors/
├── errors.go # Core error types and constructors
├── codes.go # Error code constants
├── middleware.go # Error handling middleware
└── errors_test.go # Tests
2. Define Error Structure
- Create standardized error response format:
type APIError struct {
Code string `json:"code"` // Machine-readable code (e.g., "VALIDATION_ERROR")
Message string `json:"message"` // Human-readable message
Details interface{} `json:"details,omitempty"` // Additional context (e.g., field errors)
Status int `json:"-"` // HTTP status code (not exposed in JSON)
}3. Define Error Codes
- Create error code constants in
codes.go:
const (
// General errors
CodeInternal = "INTERNAL_ERROR"
CodeNotFound = "NOT_FOUND"
CodeUnauthorized = "UNAUTHORIZED"
CodeForbidden = "FORBIDDEN"
CodeValidation = "VALIDATION_ERROR"
CodeConflict = "CONFLICT"
// Auth specific
CodeInvalidCredentials = "INVALID_CREDENTIALS"
CodeTokenExpired = "TOKEN_EXPIRED"
CodeTokenInvalid = "TOKEN_INVALID"
// User specific
CodeUserNotFound = "USER_NOT_FOUND"
CodeUserAlreadyExists = "USER_ALREADY_EXISTS"
CodeEmailAlreadyExists = "EMAIL_ALREADY_EXISTS"
)4. Error Constructors
- Implement helper functions for common errors:
// Constructor examples
func NotFound(message string) *APIError
func Unauthorized(message string) *APIError
func BadRequest(message string) *APIError
func Internal(err error) *APIError
func ValidationError(details interface{}) *APIError
func Conflict(message string) *APIError
// With details
func NotFoundWithDetails(message string, details interface{}) *APIError5. Error Response Format
Standard JSON response for all errors:
{
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": {
"email": "Invalid email format",
"password": "Password must be at least 6 characters"
}
}6. Error Handling Middleware
- Create middleware in
middleware.gothat:- Catches panics and converts to 500 errors
- Handles APIError types
- Converts unknown errors to generic internal errors
- Logs errors with appropriate levels
- Returns consistent JSON responses
Example:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last()
// Handle APIError
if apiErr, ok := err.Err.(*APIError); ok {
c.JSON(apiErr.Status, apiErr)
return
}
// Handle unknown errors
c.JSON(500, Internal(err.Err))
}
}
}7. Integration with Handlers
- Update all handlers to use new error types:
Before:
func (h *Handler) GetUser(c *gin.Context) {
user, err := h.service.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
// ...
}After:
func (h *Handler) GetUser(c *gin.Context) {
user, err := h.service.GetByID(id)
if err != nil {
_ = c.Error(errors.NotFound("User not found"))
return
}
// ...
}8. Validation Error Helpers
- Create helper for Gin validation errors:
func FromGinValidation(err error) *APIError {
var details = make(map[string]string)
for _, fieldErr := range err.(validator.ValidationErrors) {
details[fieldErr.Field()] = formatValidationError(fieldErr)
}
return ValidationError(details)
}9. Update All Handlers
- Refactor
internal/user/handler.go:- Register handler
- Login handler
- GetUser handler
- UpdateUser handler
- DeleteUser handler
- Update error responses to use new error package
- Remove
gin.H{"error": ...}patterns
10. Swagger Documentation
- Update Swagger annotations to document error codes:
// @Failure 400 {object} errors.APIError "Validation error"
// @Failure 401 {object} errors.APIError "Unauthorized - invalid credentials"
// @Failure 404 {object} errors.APIError "User not found"
// @Failure 500 {object} errors.APIError "Internal server error"- Regenerate Swagger docs:
make swag
11. Testing
- Test error constructors
- Test error middleware
- Test error serialization to JSON
- Test validation error conversion
- Add integration tests with example requests
- Test error logging
12. Documentation
- Update README.md with error handling approach
- Document all error codes in API documentation
- Add examples of error responses
- Update Postman collection with error examples
💡 Implementation Example
errors/errors.go
package errors
import "net/http"
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
Status int `json:"-"`
}
func (e *APIError) Error() string {
return e.Message
}
// Constructors
func NotFound(message string) *APIError {
return &APIError{
Code: CodeNotFound,
Message: message,
Status: http.StatusNotFound,
}
}
func BadRequest(message string) *APIError {
return &APIError{
Code: CodeValidation,
Message: message,
Status: http.StatusBadRequest,
}
}
func Unauthorized(message string) *APIError {
return &APIError{
Code: CodeUnauthorized,
Message: message,
Status: http.StatusUnauthorized,
}
}
func Internal(err error) *APIError {
return &APIError{
Code: CodeInternal,
Message: "Internal server error",
Details: err.Error(),
Status: http.StatusInternalServerError,
}
}
func ValidationError(details interface{}) *APIError {
return &APIError{
Code: CodeValidation,
Message: "Validation failed",
Details: details,
Status: http.StatusBadRequest,
}
}Usage in Handlers
// Register
if err := c.ShouldBindJSON(&req); err != nil {
_ = c.Error(errors.FromGinValidation(err))
return
}
// User not found
if user == nil {
_ = c.Error(errors.NotFound("User not found"))
return
}
// Duplicate email
if isDuplicate {
_ = c.Error(errors.Conflict("Email already exists"))
return
}
// Invalid credentials
if !validPassword {
_ = c.Error(errors.Unauthorized("Invalid email or password"))
return
}📊 Benefits Over Current Approach
| Current | With Error Package |
|---|---|
gin.H{"error": "..."} |
errors.NotFound("...") |
| Inconsistent formats | Standardized structure |
| No error codes | Machine-readable codes |
| Hard to test | Easy to test and mock |
| No error context | Details field for context |
| Manual status codes | Automatic status codes |
📚 Resources
- qiangxue/go-rest-api error handling
- Go Error Handling Best Practices
- RFC 7807 - Problem Details
- API Error Handling Best Practices
🎓 Difficulty Level
Intermediate - Requires refactoring existing handlers and understanding error propagation patterns.
🔗 Related Issues
This complements:
- Add Request Logging Middleware #8 Request Logging Middleware (errors should be logged)
- Add Rate Limiting Middleware #9 Rate Limiting (will use error codes for rate limit responses)
Note: This is a foundational improvement that will make the API more professional and easier to consume. Test thoroughly with make test and verify all endpoints return consistent error formats!
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomershacktoberfesthelp wantedExtra attention is neededExtra attention is needed