Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungWhy Claude Code keeps writing shell commands that fail on your Mac(20.09.2026 um 21:06 Uhr)
Sichere Programmierungllms.txt v2: What the Spec Says, and What 137,000 Domains Show(20.09.2026 um 21:17 Uhr)
Sicherheitslücken (CVE)NiceTryGPT: Less pattern matching. More actual hacking.(20.09.2026 um 21:19 Uhr)
IT Security VideoActivities BoF (kde2026)(20.09.2026 um 00:00 Uhr)
IT Security Toolsirdoc-app(20.09.2026 um 20:33 Uhr)
Sichere ProgrammierungWhy Claude Code keeps writing shell commands that fail on your Mac(20.09.2026 um 21:06 Uhr)
Sichere Programmierungllms.txt v2: What the Spec Says, and What 137,000 Domains Show(20.09.2026 um 21:17 Uhr)
Sicherheitslücken (CVE)NiceTryGPT: Less pattern matching. More actual hacking.(20.09.2026 um 21:19 Uhr)
IT Security VideoActivities BoF (kde2026)(20.09.2026 um 00:00 Uhr)
IT Security Toolsirdoc-app(20.09.2026 um 20:33 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Go errors: sentinel errors, error structs or fmt.Errorf — how to choose

Reagiere als Erste:r — dein Feedback zählt!

Code review on ClaudeGate. The HTTP handler that receives execution jobs was returning a 500 when the job queue was full. A colleague caught it in the logs: "this isn't an internal error, it's saturation — it should be a 503."

He was right. The problem was a single innocent line: all errors from Enqueue treated the same way, without distinguishing their nature. That's the moment you have to ask the real question: how do you structure errors so the caller can respond correctly?

The concrete bug — 500 instead of 503

The original code in the HTTP handler looked like this:

// ❌ Before — any enqueue error returns 500
if err := h.queue.Enqueue(j.ID); err != nil {
    writeError(w, http.StatusInternalServerError, "failed to enqueue job")
    return
}

"Queue full" and "internal error" are not the same thing. A server with a full queue is working correctly — it's temporarily saturated. A client receiving 500 assumes the server is broken and probably won't retry. A client receiving 503 Service Unavailable knows it can retry in a few seconds.

To distinguish between the two cases, the error needs to be identifiable. That's where sentinel errors come in.

The fix — sentinel error + errors.Is

A sentinel error is an error value declared at the package level that the caller can compare directly. That's exactly what we need here:

// In queue.go
var ErrQueueFull = errors.New("queue full")

func (q *Queue) Enqueue(jobID string) error {
    select {
    case q.jobs <- jobID:
        return nil
    default:
        return fmt.Errorf("%w: job %s", ErrQueueFull, jobID)
    }
}
// In handler.go
if err := h.queue.Enqueue(j.ID); err != nil {
    if errors.Is(err, queue.ErrQueueFull) {
        writeError(w, http.StatusServiceUnavailable, "server busy, retry later")
    } else {
        writeError(w, http.StatusInternalServerError, "failed to enqueue job")
    }
    return
}

The %w in fmt.Errorf matters: it wraps the original error inside the enriched message ("queue full: job abc-123"), and errors.Is knows how to traverse that wrapping to find ErrQueueFull. Without %w, you'd lose the ability to compare.

The 3 Go error patterns

This ClaudeGate case illustrates why Go has several ways to handle errors. Here are the three patterns, when to use each, and why.

Pattern 1: fmt.Errorf — the general case

When the caller doesn't distinguish between failure causes — it just propagates the error or logs it and moves on — fmt.Errorf is more than enough:

// ✅ The caller doesn't act differently based on error type
func processItem(ctx context.Context, item Item) error {
    result, err := fetchData(ctx, item.ID)
    if err != nil {
        return fmt.Errorf("failed to fetch data for item %s: %w", item.ID, err)
    }

    if err := store.Save(ctx, result); err != nil {
        return fmt.Errorf("failed to save result for item %s: %w", item.ID, err)
    }

    return nil
}

The %w is still useful here: it lets upper layers unwrap the error if they need to, without having to anticipate that upfront.

Pattern 2: Sentinel error — the caller distinguishes multiple cases

When the caller needs to make different decisions based on the reason for failure, it needs a comparable value. That's the role of sentinel errors:

// Declared at package level
var (
    ErrQueueFull   = errors.New("queue full")
    ErrJobNotFound = errors.New("job not found")
    ErrJobCanceled = errors.New("job canceled")
)

// Returned with enriched context
func (q *Queue) GetJob(id string) (*Job, error) {
    job, ok := q.jobs[id]
    if !ok {
        return nil, fmt.Errorf("%w: %s", ErrJobNotFound, id)
    }
    return job, nil
}

// Caller that distinguishes cases
job, err := q.GetJob(jobID)
if err != nil {
    switch {
    case errors.Is(err, ErrJobNotFound):
        http.NotFound(w, r)
    case errors.Is(err, ErrJobCanceled):
        writeError(w, http.StatusGone, "job was canceled")
    default:
        writeError(w, http.StatusInternalServerError, "unexpected error")
    }
    return
}

The standard library is full of sentinels: io.EOF, sql.ErrNoRows, context.Canceled, context.DeadlineExceeded. These are well-known values that any caller can compare directly.

Pattern 3: Error struct — the error carries data

Sometimes the caller needs more than a boolean signal — it needs structured data contained in the error itself. An error struct implementing the error interface handles that:

// Struct definition
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on field %s: %s", e.Field, e.Message)
}

// Returning it
func validatePayload(p Payload) error {
    if p.Name == "" {
        return &ValidationError{Field: "name", Message: "required"}
    }
    return nil
}

// Caller extracting the data
if err := validatePayload(p); err != nil {
    var ve *ValidationError
    if errors.As(err, &ve) {
        respondWithFieldError(w, ve.Field, ve.Message)
        return
    }
    writeError(w, http.StatusInternalServerError, "validation failed")
    return
}

It's errors.As and not errors.Is for structs: you extract the concrete type rather than comparing a value. The standard library does the same with *os.PathError (carries Op + Path + Err) or *url.Error (carries Method + URL + Err).

errors.Is vs errors.As — the difference

Both traverse the wrapping chain created by %w, but they do different things:

// errors.Is — compares values (for sentinel errors)
// Returns true if any error in the chain == ErrQueueFull
errors.Is(err, ErrQueueFull)

// errors.As — extracts a type (for error structs)
// Walks the chain, assigns the first *ValidationError found into &ve
var ve *ValidationError
errors.As(err, &ve)

The classic trap with error structs:

// ❌ Direct type assertion — doesn't traverse %w wrapping
if _, ok := err.(*ValidationError); ok {
    // Misses if the error was wrapped with fmt.Errorf("...: %w", ve)
}

// ✅ errors.As — traverses correctly
var ve *ValidationError
if errors.As(err, &ve) {
    // Works even if wrapped
}

The decision table

Situation

Pattern

The caller propagates or logs, no case distinction needed

fmt.Errorf("context: %w", err)

The caller distinguishes multiple failure causes

var ErrXxx = errors.New(...) + errors.Is

The error must carry structured data

Struct implementing error + errors.As

What not to do

Three anti-patterns that keep showing up in code reviews:

// ❌ Comparing error strings — fragile and unmaintainable
if err.Error() == "queue full" {
    // Breaks as soon as you change the error message
}

// ❌ Direct type assertion — misses wrapped errors
if _, ok := err.(*ValidationError); ok {
    // Doesn't work if the error was wrapped
}

// ❌ Sentinel error when data matters
var ErrNotFound = errors.New("not found")
// Which ID? Which resource type? The caller has no idea.
// Better: a struct that carries that information

The last point is subtle. sql.ErrNoRows works as a sentinel because the context (which query, which ID) is already known to the caller. But if the error itself needs to carry that context, a struct is more appropriate.

Standard library examples worth remembering

The Go standard library is a good guide to what works in practice over time:

  • io.EOF — sentinel, no additional data, the caller just knows reading is done
  • sql.ErrNoRows — sentinel, the caller distinguishes "no result" from "connection error"
  • context.Canceled / context.DeadlineExceeded — two distinct sentinels, the caller can react differently to each
  • *os.PathError — struct, carries Op + Path + Err: essential for displaying a useful error message
  • *url.Error — struct, carries Method + URL + Err for debugging HTTP calls

Conclusion

The rule is simple: pick the simplest pattern that meets the caller's actual needs. If the caller doesn't need to distinguish cases, fmt.Errorf is enough. If the caller needs to make different decisions, a sentinel error is the right tool. If the error needs to carry structured data, an error struct is the answer.

In the ClaudeGate case, the fix was small — a sentinel error and two lines in the handler. But the difference for API clients was significant: a 503 with a "retry later" message gives them actionable information. A generic 500 tells them nothing useful.

Go's error system is deliberately minimal. It's on us to choose the right granularity based on what the caller actually needs to know — not based on what looks cleaner to write.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Go errors: sentinel errors, error structs or fmt.Errorf — how to choose

Thematisch verwandte Begriffe: errors, sentinel, error, structs · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-93956 | A flaw has been found in olivier-ls PHP-FTS up to 1.1.2. Affected by thi…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick