Skip to main content

Echo Error Handling

Error handling is a critical aspect of building robust web applications. In this guide, you'll learn how Echo provides elegant ways to handle errors, customize error responses, and create consistent error handling patterns across your application.

Introduction to Error Handling in Echo

When building web applications, errors are inevitable. Users provide invalid input, external services go down, and unexpected situations occur. Proper error handling:

  • Provides meaningful feedback to users
  • Helps developers debug issues
  • Maintains security by not exposing sensitive information
  • Ensures a consistent user experience

Echo provides built-in error handling mechanisms that make it easy to handle these situations with minimal code.

Echo's Default Error Handler

Echo comes with a default error handler that converts errors into appropriate HTTP responses. Let's see what happens when an error occurs in an Echo application:

go
package main

import (
"net/http"
"github.com/labstack/echo/v4"
)

func main() {
e := echo.New()

// A route that returns an error
e.GET("/error", func(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Something went wrong")
})

e.Start(":1323")
}

When you access /error, Echo will return a JSON response like:

json
{
"message": "Something went wrong"
}

With a 400 Bad Request HTTP status code.

Using HTTP Error Types

Echo provides the NewHTTPError() function to create HTTP errors with specific status codes:

go
// Return a 400 Bad Request
return echo.NewHTTPError(http.StatusBadRequest, "Invalid input")

// Return a 401 Unauthorized
return echo.NewHTTPError(http.StatusUnauthorized, "Please login")

// Return a 404 Not Found
return echo.NewHTTPError(http.StatusNotFound, "Resource not found")

// Return a 500 Internal Server Error
return echo.NewHTTPError(http.StatusInternalServerError, "Server error")

Creating a Custom Error Handler

You can customize how errors are presented to users by defining your own error handler:

go
package main

import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
)

type ErrorResponse struct {
Status int `json:"status"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}

func customErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "Internal Server Error"
details := err.Error()

// Check if it's an Echo HTTPError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = fmt.Sprintf("%v", he.Message)
}

// Don't expose error details in production
if c.Echo().Debug {
c.JSON(code, ErrorResponse{
Status: code,
Message: message,
Details: details,
})
} else {
c.JSON(code, ErrorResponse{
Status: code,
Message: message,
})
}
}

func main() {
e := echo.New()
e.Debug = true

// Set custom error handler
e.HTTPErrorHandler = customErrorHandler

// A route that returns an error
e.GET("/error", func(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request parameters")
})

e.Start(":1323")
}

This custom handler formats errors consistently and only shows detailed error information when in debug mode.

Handling Common Error Scenarios

Validation Errors

When validating user input, you'll often need to return validation errors:

go
func createUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request body")
}

if u.Name == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Name is required")
}

if len(u.Password) < 8 {
return echo.NewHTTPError(http.StatusBadRequest, "Password must be at least 8 characters")
}

// Process the valid user...
return c.JSON(http.StatusCreated, u)
}

Database Errors

When working with databases, you'll want to handle errors appropriately:

go
func getUser(c echo.Context) error {
id := c.Param("id")

user, err := db.GetUser(id)
if err != nil {
if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
// Log the actual database error for debugging
log.Printf("Database error: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "Database error")
}

return c.JSON(http.StatusOK, user)
}

Authorization Errors

For authentication and authorization errors:

go
func protected(c echo.Context) error {
userID := getUserIDFromToken(c)
if userID == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "Please login to access this resource")
}

// Check if user has permission
hasAccess, err := checkUserAccess(userID, "resource")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to verify access")
}

if !hasAccess {
return echo.NewHTTPError(http.StatusForbidden, "You don't have permission to access this resource")
}

// Process the request...
return c.String(http.StatusOK, "Protected content")
}

Real-world Example: API Error Handling

Let's create a more comprehensive example of a REST API with consistent error handling:

go
package main

import (
"fmt"
"log"
"net/http"
"strings"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

// AppError represents an application error
type AppError struct {
Code int
Message string
Details interface{}
}

// Error implements the error interface
func (e AppError) Error() string {
return e.Message
}

// ErrorResponse is the structure of our error JSON response
type ErrorResponse struct {
Status int `json:"status"`
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
}

// Global error handler
func errorHandler(err error, c echo.Context) {
var (
code = http.StatusInternalServerError
message = "Internal server error"
details interface{}
)

// Handle Echo's built-in errors
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = fmt.Sprintf("%v", he.Message)
details = he.Internal
}

// Handle our custom AppError
if ae, ok := err.(AppError); ok {
code = ae.Code
message = ae.Message
details = ae.Details
}

// Log any 500 errors for investigation
if code >= 500 {
log.Printf("Server error: %v", err)
}

// In production, only return details for non-server errors
if !c.Echo().Debug && code >= 500 {
details = nil
}

if !c.Response().Committed {
if c.Request().Method == http.MethodHead {
c.NoContent(code)
} else {
c.JSON(code, ErrorResponse{
Status: code,
Message: message,
Details: details,
})
}
}
}

// Example route handlers
func getUser(c echo.Context) error {
id := c.Param("id")

// Validate ID format
if !isValidUserID(id) {
return AppError{
Code: http.StatusBadRequest,
Message: "Invalid user ID format",
Details: map[string]string{
"id": "Must be a valid UUID",
},
}
}

// Simulate database lookup
if id != "123" {
return AppError{
Code: http.StatusNotFound,
Message: "User not found",
}
}

// Return the user
return c.JSON(http.StatusOK, map[string]string{
"id": id,
"name": "John Doe",
})
}

// Helper function to validate user ID
func isValidUserID(id string) bool {
return len(id) > 0
}

func main() {
e := echo.New()
e.Debug = true

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Set custom error handler
e.HTTPErrorHandler = errorHandler

// Routes
e.GET("/users/:id", getUser)

// Start server
e.Logger.Fatal(e.Start(":1323"))
}

With this implementation:

  1. We have a custom AppError type that lets us include error details
  2. Our error handler processes both Echo's built-in errors and our custom errors
  3. We log server errors but don't expose details to clients in production
  4. We return consistent JSON responses for all errors

Best Practices for Error Handling

  1. Be specific but secure - Provide enough information to help users correct their mistakes, but don't reveal implementation details that could create security vulnerabilities.

  2. Use appropriate status codes - HTTP status codes have specific meanings:

    • 4xx codes indicate client errors (their fault)
    • 5xx codes indicate server errors (your fault)
  3. Log errors appropriately - Log serious errors for debugging, but avoid logging sensitive user data.

  4. Centralize error handling - Use a custom error handler to ensure consistent error responses.

  5. Validate early - Catch validation errors before processing the request.

  6. Handle specific errors differently - Database connection errors should be handled differently than validation errors.

Summary

Echo provides flexible error handling tools that let you create a consistent experience for your users. By implementing custom error handlers and using appropriate HTTP status codes, you can build robust web applications that gracefully handle unexpected situations.

In this guide, we've covered:

  • Echo's built-in error handling
  • Creating custom error handlers
  • Handling common error scenarios
  • Implementing a comprehensive error handling strategy
  • Best practices for web application error handling

Additional Resources

Exercises

  1. Create a custom error handler that formats errors differently based on the request's Accept header (JSON vs HTML).
  2. Implement a middleware that recovers from panics and converts them to 500 errors.
  3. Build a form validation system that returns detailed validation errors for multiple fields.
  4. Create an error logging middleware that stores errors in a database for later analysis.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)