Learn Echo - Error Handling & Logging
Series/Learn Echo/Episode 11
Episode 11 of 23

Learn Echo - Error Handling & Logging

This episode tidies up the failed-response side: HTTPError and a custom error handler, the RFC 9457 Problem Details format for consistent API errors, structured logging with slog through RequestLogger, and log integration with aggregators like Loki and ELK.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A healthy API doesn't just handle success — it designs for failure. API consumers need to know what went wrong, why, and how to fix it, in a consistent format. On the other side, developers need logs that machines can search and analyze.

Episode 11 tidies up both sides: HTTPError and a custom error handler, the RFC 9457 Problem Details format for API errors, structured logging with slog via RequestLogger, and log aggregator integration.

Understanding HTTPError

Centralized Errors with Status Codes

Echo represents errors as echo.HTTPError — an error that carries a status code, a message, and extra payload:

Creating an HTTPError
if user == nil {
	return echo.NewHTTPError(http.StatusNotFound, "user tidak ditemukan")
}

When a handler returns echo.HTTPError, Echo automatically writes the status code and message to the response. The handler doesn't need to know HTTP details — just return the error.

Filling in Additional Details

HTTPError has a Details field for contextual data, and Message can be filled with complex structures:

HTTPError with details
err := echo.NewHTTPError(http.StatusBadRequest)
err.Message = "validasi gagal"
err.Details = []string{"email harus format valid", "nama minimal 3 karakter"}
return err

The Details field is serialized into the response, giving clients information they can use to fix the request.

Custom Error Handler

e.HTTPErrorHandler

Every error — from handlers, binding, validators, or the router — eventually passes through one point: e.HTTPErrorHandler. Replace it with your own implementation for full control:

Custom error handler
e.HTTPErrorHandler = func(err error, c echo.Context) {
	he, ok := err.(*echo.HTTPError)
	if !ok {
		he = echo.NewHTTPError(http.StatusInternalServerError, "internal server error")
	}
	if err := c.JSON(he.Code, he.Message); err != nil {
		e.Logger.Error("gagal menulis respons error", "err", err)
	}
}

Non-HTTP errors are wrapped into 500 so internal information doesn't leak to the client. Logging of the original error details is done separately, not sent to the response.

RFC 9457 Problem Details

Standard API Error Format

RFC 9457 defines a consistent format for HTTP errors: a single JSON object with standard members like type, title, status, detail, and instance:

Problem Details response
{
  "type": "https://example.com/problems/validation",
  "title": "Validasi gagal",
  "status": 400,
  "detail": "Satu atau lebih field tidak valid",
  "instance": "/api/v1/users",
  "errors": ["email harus format valid"]
}

Implement it in the custom error handler by mapping HTTPError to a Problem Details structure:

Map HTTPError to Problem Details
type Problem struct {
	Type     string `json:"type"`
	Title    string `json:"title"`
	Status   int    `json:"status"`
	Detail   string `json:"detail"`
	Instance string `json:"instance"`
}
 
e.HTTPErrorHandler = func(err error, c echo.Context) {
	he, ok := err.(*echo.HTTPError)
	if !ok {
		he = echo.NewHTTPError(http.StatusInternalServerError)
	}
	problem := Problem{
		Type:     "https://example.com/problems/" + http.StatusText(he.Code),
		Title:    http.StatusText(he.Code),
		Status:   he.Code,
		Detail:   fmt.Sprint(he.Message),
		Instance: c.Path(),
	}
	if err := c.JSON(he.Code, problem); err != nil {
		e.Logger.Error("gagal menulis problem", "err", err)
	}
}

With this format, all your API errors share the same schema — easy for clients to understand and easy to map into documentation.

Structured Logging with slog

Configuring RequestLogger

In episode 3 you already used RequestLogger. In production, set the slog level and output handler according to the environment:

Setting up slog for production
level := slog.LevelInfo
if cfg.Environment == "development" {
	level = slog.LevelDebug
}
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
	Level: level,
})))

NewJSONHandler produces JSON logs that are easy for machines to parse. In development, LevelDebug gives more visibility.

Log Format and Request Context

Leverage slog inside handlers to record domain-specific context:

Contextual logging in a handler
slog.Info("user dibuat",
	"user_id", user.ID,
	"by", c.Get("user_id"),
	"trace_id", c.Response().Header().Get(echo.HeaderXRequestID),
)

Make it a habit to include a request_id in every log so a single request can be traced end to end — the foundation of observability in episode 18.

Log Aggregator Integration

Streaming Logs to an Aggregator

JSON logs on stdout are ready to be directed to an aggregator like Loki or ELK without extra code:

Docker Compose: forwarding logs to Loki
services:
  api:
    image: belajar-echo:latest
    logging:
      driver: json-file
  promtail:
    image: grafana/promtail:latest
    volumes:
      - /var/log:/var/log
    command: -config.file=/etc/promtail/config.yaml

Because the logs are already structured, the aggregator can build labels directly from fields like status and latency without manual parsing.

Closing

Episode 11 tidies up failed responses and the application's digital trail: HTTPError becomes the central error language, a custom error handler controls the final response, RFC 9457 Problem Details gives a consistent error schema, slog produces structured logs, and aggregators receive those logs without any code changes.

Key takeaways:

  • Handlers return errors; HTTPError carries the status code and message.
  • e.HTTPErrorHandler is the single control point for every error.
  • RFC 9457 provides a standard schema for API errors.
  • Don't leak internal details; wrap unknown errors into 500.
  • slog with a JSON handler produces machine-ready logs.
  • Include a request_id in every log for tracing.
  • JSON logs can be directed straight to Loki or ELK.

In episode 12 next, we'll discuss context, timeout & concurrencyc.Request().Context() for values and cancellation, goroutines inside handlers, context timeouts, state synchronization with mutex, and safe patterns when sharing data between requests.

Learn Echo - Error Handling & Logging | Learn Echo