🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
⚠️ Malware / Trojaner / VirenWindows 11 just dropped the tool ransomware abused, Microsoft says don’t restore WMIC(10.09.2026 um 20:11 Uhr)
⚠️ Malware / Trojaner / VirenWindows 11: Microsoft entfernt WMIC-Tool gegen Ransomware - ad-hoc-news.de(14.09.2026 um 07:58 Uhr)
🕵️ SicherheitslückenMicrosoft schließt Rekordzahl an Sicherheitslücken - techbook(14.09.2026 um 09:00 Uhr)
🪟 Windows TippsThe Gemini desktop app is now available for Windows(11.09.2026 um 17:06 Uhr)
🕵️ SicherheitslückenBurn Out, Or Fade Away(14.09.2026 um 14:25 Uhr)
⚠️ Malware / Trojaner / VirenWindows 11 just dropped the tool ransomware abused, Microsoft says don’t restore WMIC(10.09.2026 um 20:11 Uhr)
⚠️ Malware / Trojaner / VirenWindows 11: Microsoft entfernt WMIC-Tool gegen Ransomware - ad-hoc-news.de(14.09.2026 um 07:58 Uhr)
🕵️ SicherheitslückenMicrosoft schließt Rekordzahl an Sicherheitslücken - techbook(14.09.2026 um 09:00 Uhr)

🔧 Programmierung 🕛 vor 5 Monaten 7 Min Lesezeit
0

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

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

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:




CODE
// ❌ 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:




CODE
// 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)
}
}









CODE
// 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:




CODE
// ✅ 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:




CODE
// 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:




CODE
// 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:




CODE
// 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:




CODE
// ❌ 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:




CODE
// ❌ 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.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
The Gemini desktop app is now available for Windows
1 Quelle
Burn Out, Or Fade Away
1 Quelle
Windows 11 just dropped the tool ransomware abused, Microsoft says don’t restore WMIC
Ä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 ...