Error Handling
This page covers Go error handling patterns used throughout the agent and backend services.
Core Rules
Always Wrap Errors
Every error must be wrapped with context describing what operation failed:
host, err := s.store.GetHost(ctx, id)
if err != nil {
return nil, fmt.Errorf("getting host by id: %w", err)
}
Never Discard Errors
Every error return value must be checked. If an error is intentionally ignored (e.g., in a deferred Close call), make it explicit:
defer func() { _ = db.Close() }()
Use errors.Is and errors.As
Check error types with errors.Is() and errors.As(). Never compare error strings:
// Correct
if errors.Is(err, store.ErrNotFound) {
return nil, fmt.Errorf("host not found: %w", err)
}
// Wrong -- do not do this
if err.Error() == "not found" {
// ...
}
Log at the Top Level
Log errors at the boundary (handler or worker), not deep in the call stack. Functions in the store, domain, or utility layers should return errors, not log them.
Do Not Log and Return
Choose one: either log the error and handle it, or return it to the caller. Doing both leads to duplicate log entries.
// Wrong -- logs and returns the same error
if err != nil {
slog.Error("failed to get host", "error", err)
return fmt.Errorf("getting host: %w", err)
}
// Correct -- return to caller, let the handler log it
if err != nil {
return fmt.Errorf("getting host: %w", err)
}
Sentinel Errors
The store package defines sentinel errors for common failure cases:
var (
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
ErrInvalidInput = errors.New("invalid input")
)
These are used throughout the store layer and checked by API handlers to determine the appropriate HTTP response.
API Error Response Mapping
API handlers map sentinel errors to HTTP status codes and structured error responses:
| Sentinel Error | HTTP Status | Error Code |
|---|---|---|
ErrInvalidInput | 400 Bad Request | bad_request |
ErrNotFound | 404 Not Found | not_found |
ErrConflict | 409 Conflict | conflict |
| Unexpected error | 500 Internal Server Error | internal_error |
Error Response Format
All API errors return a consistent JSON structure:
{
"error": {
"code": "not_found",
"message": "host not found"
}
}
Handler Example
func (h *Handler) GetHost(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
respondError(w, http.StatusBadRequest, "bad_request", "invalid host id")
return
}
host, err := h.store.GetHost(r.Context(), id)
if err != nil {
switch {
case errors.Is(err, store.ErrNotFound):
respondError(w, http.StatusNotFound, "not_found", "host not found")
default:
slog.Error("failed to get host", "error", err, "host_id", id)
respondError(w, http.StatusInternalServerError, "internal_error", "internal error")
}
return
}
respondJSON(w, http.StatusOK, host)
}