Echo Error Handling
Error handling is a critical aspect of developing robust web applications. When building APIs and web services with Echo, proper error handling ensures your application can gracefully recover from unexpected situations while providing meaningful feedback to users. This guide will walk you through the best practices for error handling in Echo.
Introduction to Error Handling in Echo
Echo provides a flexible error handling framework that allows you to:
- Centralize error handling logic
- Customize error responses
- Log errors appropriately
- Handle different error types distinctly
Effective error handling not only improves user experience but also helps developers diagnose and fix issues more quickly.
Echo's Default Error Handler
Echo comes with a default error handler that returns a JSON response with the error message and status code. Let's look at how it works:
// Default error handler in Echo
func defaultHTTPErrorHandler(err error, c echo.Context) {
he, ok := err.(*echo.HTTPError)
if ok {
if he.Internal != nil {
if herr, ok := he.Internal.(*echo.HTTPError); ok {
he = herr
}
}
} else {
he = &echo.HTTPError{
Code: http.StatusInternalServerError,
Message: http.StatusText(http.StatusInternalServerError),
}
}
// Send response
code := he.Code
message := he.Message
if m, ok := he.Message.(string); ok {
message = map[string]interface{}{"message": m}
}
if c.Request().Method == http.MethodHead {
err = c.NoContent(he.Code)
} else {
err = c.JSON(code, message)
}
if err != nil {
c.Logger().Error(err)
}
}
This default handler is suitable for many applications, but as your app grows, you'll likely need to customize it.
Types of Errors in Echo Applications
In Echo applications, you'll typically encounter several types of errors:
- HTTP Errors: Built-in Echo errors with HTTP status codes
- Validation Errors: Errors that occur during request validation
- Database Errors: Errors from database operations
- External Service Errors: Issues when calling third-party services
- Application Logic Errors: Custom errors specific to your business logic
Creating Custom HTTP Errors
Echo makes it easy to create custom HTTP errors:
// Creating a custom HTTP error
func getUser(c echo.Context) error {
id := c.Param("id")
// User not found scenario
if id == "999" {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
// Server error scenario
if id == "0" {
return echo.NewHTTPError(http.StatusInternalServerError, "Database error")
}
// Success case
return c.JSON(http.StatusOK, map[string]string{"id": id, "name": "John Doe"})
}
Custom Error Handling Middleware
One of Echo's strengths is its middleware system, which is perfect for centralized error handling. Here's how to create a custom error handler:
package main
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// Custom error handler
e.HTTPErrorHandler = customErrorHandler
// Routes
e.GET("/api/users/:id", getUser)
e.Logger.Fatal(e.Start(":8080"))
}
func customErrorHandler(err error, c echo.Context) {
var (
code = http.StatusInternalServerError
message interface{} = "Internal server error"
)
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = he.Message
if he.Internal != nil {
// Log the internal error details
fmt.Println("Internal error:", he.Internal)
}
}
// Log the error
c.Logger().Error(err)
// Don't send error details in production
if c.Echo().Debug {
message = err.Error()
}
// Return JSON response with error details
if !c.Response().Committed {
if c.Request().Method == http.MethodHead {
c.NoContent(code)
} else {
c.JSON(code, map[string]interface{}{
"error": message,
"code": code,
"status": http.StatusText(code),
})
}
}
}
func getUser(c echo.Context) error {
id := c.Param("id")
if id == "999" {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
return c.JSON(http.StatusOK, map[string]string{"id": id, "name": "John Doe"})
}
Handling Validation Errors
When validating requests, properly handling validation errors improves API usability:
package main
import (
"net/http"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
type User struct {
Name string `json:"name" validate:"required,min=3"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"min=18"`
}
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
func main() {
e := echo.New()
// Set custom validator
e.Validator = &CustomValidator{validator: validator.New()}
// Custom error handler with validation support
e.HTTPErrorHandler = customErrorHandler
// Routes
e.POST("/api/users", createUser)
e.Logger.Fatal(e.Start(":8080"))
}
func createUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request payload")
}
if err := c.Validate(u); err != nil {
// Return validation error
return echo.NewHTTPError(http.StatusBadRequest, formatValidationError(err))
}
// Process the valid user...
return c.JSON(http.StatusCreated, u)
}
func formatValidationError(err error) map[string]interface{} {
errors := make(map[string]interface{})
for _, err := range err.(validator.ValidationErrors) {
field := err.Field()
tag := err.Tag()
value := err.Value()
errors[field] = map[string]interface{}{
"tag": tag,
"value": value,
"error": getErrorMessage(field, tag, value),
}
}
return map[string]interface{}{
"message": "Validation failed",
"details": errors,
}
}
func getErrorMessage(field, tag string, value interface{}) string {
switch tag {
case "required":
return field + " is required"
case "email":
return field + " must be a valid email address"
case "min":
return field + " does not meet minimum requirements"
default:
return field + " is invalid"
}
}
func customErrorHandler(err error, c echo.Context) {
var (
code = http.StatusInternalServerError
message interface{} = "Internal server error"
)
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = he.Message
}
// Log the error
c.Logger().Error(err)
// Return JSON response
if !c.Response().Committed {
c.JSON(code, map[string]interface{}{
"error": message,
})
}
}
Sample Request and Response
For a validation error, the request and response might look like:
Request:
{
"name": "Jo",
"email": "not-an-email",
"age": 16
}
Response:
{
"error": {
"message": "Validation failed",
"details": {
"Name": {
"tag": "min",
"value": "Jo",
"error": "Name does not meet minimum requirements"
},
"Email": {
"tag": "email",
"value": "not-an-email",
"error": "Email must be a valid email address"
},
"Age": {
"tag": "min",
"value": 16,
"error": "Age does not meet minimum requirements"
}
}
}
}
Logging Errors
Proper error logging is crucial for debugging and monitoring. Echo's built-in logger can be extended for better error logging:
func customErrorMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Process the request
err := next(c)
if err != nil {
// Get request information
req := c.Request()
// Log detailed error information
c.Logger().Errorf(
"ERROR: %v | METHOD: %s | URL: %s | IP: %s",
err,
req.Method,
req.URL.Path,
c.RealIP(),
)
}
return err
}
}
You can register this middleware globally:
e := echo.New()
e.Use(customErrorMiddleware)
Best Practices for Error Handling in Echo
-
Use Specific HTTP Status Codes: Choose appropriate HTTP status codes for different error scenarios:
- 400 Bad Request: Client sent an invalid request
- 401 Unauthorized: Authentication required
- 403 Forbidden: Client doesn't have permission
- 404 Not Found: Resource doesn't exist
- 422 Unprocessable Entity: Validation errors
- 500 Internal Server Error: Unexpected server issues
-
Hide Implementation Details in Production: Don't expose stack traces or internal error messages to the client in production.
-
Consistent Error Response Format: Maintain a consistent structure for all error responses.
-
Log Errors with Context: Include relevant details when logging errors (request path, method, user info).
-
Graceful Recovery: Use middleware to recover from panics.
Real-World Example: API Error Handling
Let's build a more comprehensive error handling solution for a REST API:
package main
import (
"fmt"
"net/http"
"runtime"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// AppError represents a custom application error
type AppError struct {
Code int `json:"-"`
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
Internal error `json:"-"` // Not exposed to clients
}
func (e *AppError) Error() string {
if e.Internal != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Internal)
}
return e.Message
}
// Common application errors
var (
ErrInvalidInput = func(details interface{}) *AppError {
return &AppError{
Code: http.StatusBadRequest,
Message: "Invalid input provided",
Details: details,
}
}
ErrNotFound = func(resource string) *AppError {
return &AppError{
Code: http.StatusNotFound,
Message: fmt.Sprintf("%s not found", resource),
}
}
ErrUnauthorized = &AppError{
Code: http.StatusUnauthorized,
Message: "Authentication required",
}
ErrForbidden = &AppError{
Code: http.StatusForbidden,
Message: "Access denied",
}
ErrInternal = func(err error) *AppError {
return &AppError{
Code: http.StatusInternalServerError,
Message: "An internal error occurred",
Internal: err,
}
}
)
func main() {
e := echo.New()
e.Debug = true // Set to false in production
// Middleware
e.Use(middleware.Recover())
e.Use(middleware.Logger())
// Custom error handler
e.HTTPErrorHandler = errorHandler
// Routes
e.GET("/api/users/:id", getUserHandler)
e.POST("/api/users", createUserHandler)
e.GET("/api/admin", adminOnlyHandler)
e.Logger.Fatal(e.Start(":8080"))
}
func errorHandler(err error, c echo.Context) {
// Default error information
code := http.StatusInternalServerError
message := "Internal Server Error"
details := map[string]interface{}{}
// Get stack trace
var stackTrace string
if c.Echo().Debug {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
stackTrace = string(buf[:n])
}
// Handle different types of errors
switch e := err.(type) {
case *AppError:
// Our custom application error
code = e.Code
message = e.Message
if e.Details != nil {
details = map[string]interface{}{
"info": e.Details,
}
}
// Log internal error if present
if e.Internal != nil {
c.Logger().Errorf("Internal error: %v", e.Internal)
if c.Echo().Debug {
details["internal"] = e.Internal.Error()
}
}
case *echo.HTTPError:
// Echo's built-in errors
code = e.Code
message = fmt.Sprintf("%v", e.Message)
default:
// Unexpected errors
c.Logger().Errorf("Unhandled error: %v", err)
if c.Echo().Debug {
message = err.Error()
}
}
// Log error with request details
c.Logger().Errorf(
"Error: %v | Code: %d | Path: %s | Method: %s | Stack: %s",
message,
code,
c.Request().URL.Path,
c.Request().Method,
stackTrace,
)
// Send error response if not already sent
if !c.Response().Committed {
response := map[string]interface{}{
"error": message,
}
if len(details) > 0 && (code < 500 || c.Echo().Debug) {
response["details"] = details
}
c.JSON(code, response)
}
}
func getUserHandler(c echo.Context) error {
id := c.Param("id")
// Simulate user not found
if id == "999" {
return ErrNotFound("User")
}
// Simulate internal error
if id == "0" {
dbErr := fmt.Errorf("database connection failed")
return ErrInternal(dbErr)
}
// Success case
return c.JSON(http.StatusOK, map[string]string{
"id": id,
"name": "John Doe",
"email": "[email protected]",
})
}
func createUserHandler(c echo.Context) error {
// Simulate validation error
return ErrInvalidInput(map[string]string{
"email": "must be a valid email address",
"age": "must be at least 18",
})
}
func adminOnlyHandler(c echo.Context) error {
// Simulate authorization error
isAdmin := false
if !isAdmin {
return ErrForbidden
}
return c.String(http.StatusOK, "Welcome, Admin!")
}
Example Request/Response Scenarios
1. User Not Found (404)
Request:
GET /api/users/999
Response:
{
"error": "User not found"
}
2. Invalid Input (400)
Request:
POST /api/users
Response:
{
"error": "Invalid input provided",
"details": {
"info": {
"email": "must be a valid email address",
"age": "must be at least 18"
}
}
}
3. Internal Server Error (500)
Request:
GET /api/users/0
Response in Production:
{
"error": "An internal error occurred"
}
Response in Debug mode:
{
"error": "An internal error occurred",
"details": {
"internal": "database connection failed"
}
}
Recovering from Panics
Echo includes middleware to recover from panics, but you can also customize it:
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
StackSize: 4 << 10, // 4 KB
DisableStackAll: false,
DisablePrintStack: false,
LogLevel: log.ERROR,
LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
// Custom logic for handling panic recovery
c.Logger().Errorf("PANIC: %v\n%s", err, stack)
// Return a generic error to the client
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "The server encountered an unexpected condition",
})
},
}))
Summary
Effective error handling is essential for building robust Echo applications. By implementing proper error handling:
- Your API becomes more predictable and easier to use
- Debugging becomes more straightforward
- Production errors are properly contained without leaking sensitive information
- User experience improves with meaningful error messages
Echo provides flexible tools for error handling that you can adapt to your specific application needs. Remember that good error handling is not just about capturing errors but also about providing clear feedback and maintaining security.
Additional Resources
Exercises
- Create a custom error handler that logs errors to a file.
- Implement a middleware that tracks error frequency and can detect potential attacks.
- Build a validation system that returns user-friendly error messages for common validation issues.
- Create a "development" vs "production" error handling mode that shows detailed errors only in development.
- Implement a custom recovery middleware that sends alerts for critical errors.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)