Skip to main content

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 ErrorHTTP StatusError Code
ErrInvalidInput400 Bad Requestbad_request
ErrNotFound404 Not Foundnot_found
ErrConflict409 Conflictconflict
Unexpected error500 Internal Server Errorinternal_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)
}