⚠️ Malware / Trojaner / Viren9 Proofpoint alternatives. Pros & cons of the leading options(24.08.2026 um 11:27 Uhr)
⚠️ Malware / Trojaner / VirenWhat the DfE’s cyber security update means for multi-academy trusts(24.08.2026 um 19:13 Uhr)
⚠️ Malware / Trojaner / VirenBuilding a ransomware decision tree before the call comes in(11.09.2026 um 07:30 Uhr)
🕵️ SicherheitslückenAutomox Mitigation Worklets cut endpoint exposure to unpatchable flaws(11.09.2026 um 09:48 Uhr)
⚠️ Malware / Trojaner / VirenFake Codex Download Uses Google Sites to Deliver macOS Malware(24.08.2026 um 17:00 Uhr)
⚠️ Malware / Trojaner / VirenFake Minecraft Clients Deliver WeedHack Malware Despite Infrastructure Takedown(25.08.2026 um 12:30 Uhr)
🕵️ SicherheitslückenFour in Five AI Tools Run with No IT Oversight, New Research Finds(26.08.2026 um 15:00 Uhr)
⚠️ Malware / Trojaner / VirenTortoiseshell Expands Malware Toolset With New Backdoor, SSH Tunnel(26.08.2026 um 16:30 Uhr)
⚠️ Malware / Trojaner / Viren9 Proofpoint alternatives. Pros & cons of the leading options(24.08.2026 um 11:27 Uhr)
⚠️ Malware / Trojaner / VirenWhat the DfE’s cyber security update means for multi-academy trusts(24.08.2026 um 19:13 Uhr)
⚠️ Malware / Trojaner / VirenBuilding a ransomware decision tree before the call comes in(11.09.2026 um 07:30 Uhr)
🕵️ SicherheitslückenAutomox Mitigation Worklets cut endpoint exposure to unpatchable flaws(11.09.2026 um 09:48 Uhr)
⚠️ Malware / Trojaner / VirenFake Codex Download Uses Google Sites to Deliver macOS Malware(24.08.2026 um 17:00 Uhr)
⚠️ Malware / Trojaner / VirenFake Minecraft Clients Deliver WeedHack Malware Despite Infrastructure Takedown(25.08.2026 um 12:30 Uhr)
🕵️ SicherheitslückenFour in Five AI Tools Run with No IT Oversight, New Research Finds(26.08.2026 um 15:00 Uhr)
⚠️ Malware / Trojaner / VirenTortoiseshell Expands Malware Toolset With New Backdoor, SSH Tunnel(26.08.2026 um 16:30 Uhr)

🔧 Programmierung 🕛 vor 5 Monaten 7 Min Lesezeit
0

Designing a safe error handling package in Go: safe by default

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

Error handling in Go is simple. Sometimes too simple. A fmt.Errorf("pq: no rows in result set") bubbling up to an HTTP handler, and suddenly SQL internals are exposed in a 500 response. No exotic vulnerability required — just an err.Error() in the wrong place.



This problem is systemic. It doesn't stem from a lack of discipline, but from the absence of a boundary in the type system. Nothing in the error interface distinguishes what is safe to expose from what is not.






The concrete problem



Here is the classic leak pattern. A handler that serves err.Error() directly in its JSON response, and a repository layer that wraps errors with technical context:




CODE
// HTTP handler
func (h *Handler) GetOrder(c echo.Context) error {
order, err := h.service.FindByID(ctx, id)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(), // ← LEAK
})
}
return c.JSON(http.StatusOK, order)
}

// repository layer
func (r *Repo) FindByID(ctx context.Context, id string) (*Order, error) {
var o Order
err := r.db.GetContext(ctx, &o, "SELECT * FROM orders WHERE id = $1", id)
if err != nil {
return nil, fmt.Errorf("orders.FindByID: %w", err) // standard wrapping
}
return &o, nil
}






Response received by the client:




CODE
{"error": "orders.FindByID: pq: no rows in result set"}






The table name, PostgreSQL driver, and nature of the SQL error are publicly exposed. Not catastrophic in isolation — but exactly the kind of information an attacker uses to refine an injection or target an attack surface.






What existing solutions offer (and their limits)



Solution



Approach



Limitation



cockroachdb/errors



Exhaustive package, stack traces, rich wrapping



~15k lines, 8 sub-packages. Disproportionate for a business microservice.



upspin pattern (Rob Pike)



Error type with Kind, Op, Err



Pre-Go 1.13, no slog, and errors.Is() traverses the full chain.



hashicorp pattern



UserError interface with GetMessage()



Relies on developer discipline, not the type system.



The real question: can we design a type where err.Error() always returns something safe, without any convention to remember?






The safeerr design



Five principles structure the package:




  1. Safe by default: Error() returns only the user-facing message.

  2. No Unwrap(): the introspection chain is intentionally broken. errors.Is() never traverses the internal cause. A deliberate security choice.

  3. slog.LogValuer: technical details are exposed only through structured logs.

  4. Immutable builder pattern: WithMsg(), WrapCause() always return a new instance — sentinels are never mutated.

  5. Centralized HTTP mapping: a single point maps ErrorKind values to HTTP status codes.






The sentinels






CODE
var (
ErrNotFound = New("NOT_FOUND", KindNotFound, http.StatusNotFound, "not found")
ErrUnauthorized = New("UNAUTHORIZED", KindUnauthorized, http.StatusUnauthorized, "unauthorized")
ErrInvalidInput = New("INVALID_INPUT", KindInvalidInput, http.StatusUnprocessableEntity, "invalid input")
ErrInvalidState = New("INVALID_STATE", KindInvalidState, http.StatusConflict, "invalid state")
ErrInternal = New("INTERNAL", KindInternal, http.StatusInternalServerError, "internal error")
)









The immutable builder pattern






CODE
func (e *AppErr) WithMsg(msg string) *AppErr {
return &AppErr{code: e.code, kind: e.kind, status: e.status, msg: msg, cause: e.cause}
}

func (e *AppErr) WrapCause(cause error, msg string) *AppErr {
return &AppErr{code: e.code, kind: e.kind, status: e.status, msg: msg, cause: cause}
}






Each call creates a new instance. Sentinels declared as var are never modified — no risk of shared mutation across goroutines.






slog.LogValuer






CODE
func (e *AppErr) LogValue() slog.Value {
attrs := []slog.Attr{
slog.String("code", e.code),
slog.String("msg", e.msg),
}
if e.cause != nil {
attrs = append(attrs, slog.String("cause", e.cause.Error()))
}
return slog.GroupValue(attrs...)
}






Resulting log:




CODE
level=ERROR msg="order processing failed" err.code=NOT_FOUND err.msg="order not found" err.cause="pq: no rows in result set"






Technical details — driver, query, table — remain in the logs. Never in the HTTP response.






Centralized HTTP mapping






CODE
func Error(c echo.Context, err error) error {
var appErr *safeerr.AppErr
if errors.As(err, &appErr) {
return c.JSON(appErr.Status(), map[string]string{
"error": appErr.Error(),
})
}
slog.Error("unhandled error reaching handler", "err", err)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal error"})
}






A single place in the codebase knows how to translate an error into an HTTP response. If an untyped error reaches this point, it is logged and returns a generic message.






Before / after



Before — technical errors bubble up raw to the handler:




CODE
// service — raw technical error propagated
func (s *Service) ProcessOrder(ctx context.Context, cmd ProcessOrderCmd) error {
order, err := s.repo.FindOrder(ctx, cmd.OrderID)
if err != nil {
return fmt.Errorf("FindOrder: %w", err)
}
if order.ContractRef == "" {
return errors.New("order has no contract reference")
}
return nil
}






After — explicit conversion at the service boundary:




CODE
// service — explicit conversion at the boundary
func (s *Service) ProcessOrder(ctx context.Context, cmd ProcessOrderCmd) error {
order, err := s.repo.FindOrder(ctx, cmd.OrderID)
if err != nil {
return safeerr.ErrNotFound.WrapCause(err, "order not found")
}
if order.ContractRef == "" {
return safeerr.ErrInvalidState.WithMsg("order has no contract reference")
}
return nil
}

// handler
func (h *Handler) ProcessOrder(c echo.Context) error {
if err := h.service.ProcessOrder(ctx, cmd); err != nil {
return render.Error(c, err)
}
return c.JSON(http.StatusOK, nil)
}






HTTP response: {"error": "order not found"} — the SQL cause stays in the logs, never in the response.






The full package (~130 lines)






CODE
package safeerr

import "log/slog"

type ErrorKind int

const (
KindNotFound ErrorKind = iota
KindUnauthorized
KindForbidden
KindInvalidInput
KindInvalidState
KindConflict
KindInternal
)

type AppErr struct {
code string
kind ErrorKind
status int
msg string
cause error
}

func New(code string, kind ErrorKind, status int, msg string) *AppErr {
return &AppErr{code: code, kind: kind, status: status, msg: msg}
}

func (e *AppErr) Error() string { return e.msg }
func (e *AppErr) Code() string { return e.code }
func (e *AppErr) Kind() ErrorKind { return e.kind }
func (e *AppErr) Status() int { return e.status }
func (e *AppErr) Cause() error { return e.cause }

func (e *AppErr) WithMsg(msg string) *AppErr {
return &AppErr{code: e.code, kind: e.kind, status: e.status, msg: msg, cause: e.cause}
}

func (e *AppErr) WrapCause(cause error, msg string) *AppErr {
return &AppErr{code: e.code, kind: e.kind, status: e.status, msg: msg, cause: cause}
}

func (e *AppErr) WithCause(cause error) *AppErr {
return &AppErr{code: e.code, kind: e.kind, status: e.status, msg: e.msg, cause: cause}
}

func (e *AppErr) LogValue() slog.Value {
attrs := []slog.Attr{
slog.String("code", e.code),
slog.String("msg", e.msg),
}
if e.cause != nil {
attrs = append(attrs, slog.String("cause", e.cause.Error()))
}
return slog.GroupValue(attrs...)
}









Honest trade-offs




  • No stack traces. AppErr does not capture the goroutine stack. Structured logs with business context compensate in most cases, but for deep post-mortem debugging this is a real limitation.

  • WrapCause can leak if misused. WrapCause(err, err.Error()) breaks the safety guarantee — the user-facing message becomes the technical one. Discipline is still required on this specific point.

  • The conversion cost. Every technical error must be converted to an AppErr at the service boundary. This is intentional friction — on a project with a hundred error sites, that's code to write and maintain.

  • errors.Is() no longer works on the cause. If an upper layer wants to test errors.Is(err, sql.ErrNoRows), it cannot. Acceptable if the sentinel taxonomy is expressive enough, problematic otherwise.






Conclusion



The real value of this package is not in the 130 lines. It's in the boundary it makes explicit between what is safe to expose and what is not. That boundary already existed in any well-designed application — but it relied on discipline, on documented conventions, on careful reviews.



Moving this constraint into the type system has a cost: explicit conversion at every boundary. In return: an accidental err.Error() in an HTTP response never leaks anything, regardless of who wrote the code.



For an internal library shared across multiple microservices in the same stack, it's an investment that pays off quickly.

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
2 Quellen
Proofpoint Brings OpenAI GPT Cyber Models into Security Operations to Help Defenders Investigate Threats Faster
1 Quelle
OpenAI: Hugging Face Incident a “Warning Shot” to the World
1 Quelle
Window to Tackle Surge in AI-Enabled Cyber Attacks Narrowing, Tech Giants Warn
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Designing a safe error handling package in Go: safe by default

Thematisch verwandte Begriffe: Designing, safe, error, handling · 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 ...