Skip to main content

Echo Not Found Handler

Introduction

When building web applications, handling requests for non-existent routes is just as important as serving valid ones. In Echo, when a client requests a route that doesn't exist, the framework automatically returns a "404 Not Found" response. However, Echo provides flexible ways to customize this behavior through what's called a "Not Found Handler."

In this tutorial, we'll learn how to create custom 404 responses that align with your application's design and provide a better user experience when users encounter missing routes.

Default Behavior

By default, when Echo can't match a request to any defined routes, it responds with a plain text "404 Not Found" message and sets the HTTP status code to 404. Let's see what happens with the default behavior:

go
package main

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

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

// Define only one route
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})

e.Start(":8080")
}

If you make a request to /nonexistent-route, you'll get a plain response like:

404 page not found

Customizing the Not Found Handler

Echo allows you to replace the default not found handler with your own, letting you customize the response format, add logging, or even attempt to recover from the situation.

Basic Custom Not Found Handler

Here's how to register a custom handler for 404 errors:

go
package main

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

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

// Define your routes
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})

// Custom not found handler
e.NotFoundHandler = func(c echo.Context) error {
return c.JSON(http.StatusNotFound, map[string]string{
"message": "Sorry, we couldn't find the page you're looking for.",
"requestPath": c.Request().URL.Path,
})
}

e.Start(":8080")
}

Now if you request /nonexistent-route, you'll get a JSON response:

json
{
"message": "Sorry, we couldn't find the page you're looking for.",
"requestPath": "/nonexistent-route"
}

HTML Response for Not Found Routes

For web applications serving HTML content, you might want to provide a custom 404 page:

go
package main

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

type Template struct {
templates *template.Template
}

func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}

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

// Configure template renderer
t := &Template{
templates: template.Must(template.ParseGlob("templates/*.html")),
}
e.Renderer = t

// Define routes
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})

// Custom HTML not found handler
e.NotFoundHandler = func(c echo.Context) error {
return c.Render(http.StatusNotFound, "404.html", map[string]interface{}{
"path": c.Request().URL.Path,
})
}

e.Start(":8080")
}

The 404.html template might look like:

html
<!DOCTYPE html>
<html>
<head>
<title>Page Not Found</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding-top: 50px;
}
.error-code {
font-size: 72px;
color: #e74c3c;
}
</style>
</head>
<body>
<div class="error-code">404</div>
<h1>Page Not Found</h1>
<p>We couldn't find the page: <code>{{.path}}</code></p>
<p><a href="/">Go back to homepage</a></p>
</body>
</html>

Advanced Not Found Handler Techniques

Logging Not Found Routes

You might want to log 404 errors to identify possible issues or broken links:

go
e.NotFoundHandler = func(c echo.Context) error {
// Log the 404 error
e.Logger.Infof("404 Error: %s %s from %s",
c.Request().Method,
c.Request().URL.Path,
c.Request().RemoteAddr)

return c.JSON(http.StatusNotFound, map[string]string{
"message": "Resource not found",
})
}

Suggesting Alternative Routes

You could implement a handler that suggests similar routes when a user encounters a 404:

go
package main

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

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

// Define routes
e.GET("/users", handleUsers)
e.GET("/products", handleProducts)
e.GET("/orders", handleOrders)

// Custom not found handler with suggestions
e.NotFoundHandler = func(c echo.Context) error {
path := c.Request().URL.Path
suggestions := findSimilarRoutes(path, []string{"/users", "/products", "/orders"})

return c.JSON(http.StatusNotFound, map[string]interface{}{
"message": "Resource not found",
"path": path,
"didYouMean": suggestions,
})
}

e.Start(":8080")
}

func findSimilarRoutes(path string, routes []string) []string {
var suggestions []string
for _, route := range routes {
if strings.Contains(route, path) || strings.Contains(path, route) {
suggestions = append(suggestions, route)
}
}
return suggestions
}

func handleUsers(c echo.Context) error {
return c.String(http.StatusOK, "Users page")
}

func handleProducts(c echo.Context) error {
return c.String(http.StatusOK, "Products page")
}

func handleOrders(c echo.Context) error {
return c.String(http.StatusOK, "Orders page")
}

If you make a request to /user (missing the 's'), you'd get:

json
{
"message": "Resource not found",
"path": "/user",
"didYouMean": ["/users"]
}

Real-world Application: API Version Fallback

In a real-world API, you might have multiple versions. Using a custom not found handler, you could redirect requests from newer, non-existent API endpoints to the latest supported version:

go
package main

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

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

// API v1 routes
v1 := e.Group("/v1")
v1.GET("/users", getV1Users)

// API v2 routes - more endpoints than v1
v2 := e.Group("/v2")
v2.GET("/users", getV2Users)
v2.GET("/users/:id", getV2UserByID)

// Custom not found handler with API version fallback
e.NotFoundHandler = func(c echo.Context) error {
path := c.Request().URL.Path

// Check if this is a v3 request (which doesn't exist yet)
re := regexp.MustCompile(`^/v3/(.*)$`)
if matches := re.FindStringSubmatch(path); len(matches) > 0 {
// Try to fallback to v2 version of the same endpoint
v2Path := "/v2/" + matches[1]
c.Request().URL.Path = v2Path

// Note: In a real application, you'd redirect or
// use proper middleware for this logic
return e.Router().Find(c.Request().Method, v2Path, c)(c)
}

return c.JSON(http.StatusNotFound, map[string]string{
"error": "Endpoint not found",
"message": "The requested resource does not exist",
})
}

e.Start(":8080")
}

func getV1Users(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"version": "v1",
"users": []string{"Alice", "Bob"},
})
}

func getV2Users(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"version": "v2",
"users": []map[string]interface{}{
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
},
})
}

func getV2UserByID(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]interface{}{
"version": "v2",
"id": id,
"name": "User " + id,
})
}

Note: The fallback approach shown here is simplified for demonstration. In production systems, you'd likely want to use proper middleware or a more sophisticated routing strategy.

Summary

Custom not found handlers in Echo are powerful tools that allow you to:

  1. Create user-friendly 404 error pages or responses
  2. Log missing endpoints to detect broken links
  3. Suggest alternative routes when a user hits a non-existent path
  4. Implement intelligent fallbacks for API versioning or legacy routes

By customizing how your application responds to requests for non-existent routes, you can improve user experience, provide better diagnostics, and handle complex routing scenarios that go beyond simple path matching.

Additional Resources and Exercises

Resources

Exercises

  1. Create a not found handler that returns different response formats (JSON/HTML/XML) based on the request's Accept header.

  2. Implement a not found handler that keeps track of the most frequently requested non-existent routes and logs them for analysis.

  3. Build a system that displays a maintenance page for specific routes that are temporarily unavailable, while showing a standard 404 page for truly non-existent routes.

  4. Create a not found handler that automatically redirects legacy URLs to their new counterparts using a mapping configuration.



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