Go-errors die bruikbaar blijven aan de API-grens
Een praktische foutstrategie in Go voor context, stabiele betekenis, HTTP-responses, één logmoment en gedragstests.
Go maakt errors zichtbaar in normale control flow, maar daarmee zijn ze niet automatisch bruikbaar. Problemen ontstaan wanneer iedere laag dezelfde fout logt, callers strings vergelijken, databasefouten in HTTP-responses lekken of een gewrapte implementatiedetail ongemerkt onderdeel van het packagecontract wordt. Ik gebruik een klein foutvocabulaire, context binnen de applicatie en één bewuste vertaling aan de grens.
Voeg context toe die zegt wat mislukte
Een fout als connection refused mist operatie en onderwerp. Iedere laag voegt alleen kennis toe die daar beschikbaar is: account 42 laden, invoice-response decoden of idempotency-record opslaan. Berichten blijven kort en beginnen met een kleine-letter-operatie, zodat een wrapped chain als één zin leest.
Wrappen met %w bewaart de onderliggende fout voor errors.Is en errors.As. Dat is nuttig wanneer de caller erop mag reageren, maar het is ook een API-beslissing. Als een repository sql.ErrNoRows exposeert raken callers gekoppeld aan database/sql. Ik vertaal dat meestal naar een domeinbrede ErrNotFound.
var ErrNotFound = errors.New("not found")
func (r *Repository) Account(ctx context.Context, id string) (Account, error) {
account, err := queryAccount(ctx, r.db, id)
if errors.Is(err, sql.ErrNoRows) {
return Account{}, fmt.Errorf("account %s: %w", id, ErrNotFound)
}
if err != nil {
return Account{}, fmt.Errorf("query account %s: %w", id, err)
}
return account, nil
}Gebruik Is voor categorieën en As voor details
errors.Is bepaalt of een error chain een betekenis bevat, zoals not found, conflict of cancellation. errors.As haalt een specifiek fouttype op wanneer de caller gestructureerde details nodig heeft. Directe equality en type assertions bekijken alleen de buitenste waarde en breken zodra een laag context toevoegt.
Sentinel errors gebruik ik voor een klein aantal stabiele condities zonder extra velden. Voor validatievelden, retrytijd of upstreamstatus is een typed error duidelijker. Niet iedere fout krijgt een eigen type; onverwachte fouten hebben meestal alleen operationele context en hun oorzaak nodig.
- errors.Is voor stabiele categorieën
- errors.As voor gestructureerde details
- %w alleen wanneer callers de oorzaak mogen gebruiken
- %v of vertaling voor implementatiedetails
- Neem nooit beslissingen op basis van fouttekst
Houd HTTP-kennis uit domeincode
Een servicemethode retourneert geen HTTP-statuscode. Vandaag komt hij uit een handler, morgen uit een queueworker. De handler vertaalt applicatiebetekenis naar protocol: not found naar 404, ongeldige invoer naar 400 of 422 volgens het contract, conflict naar 409 en onverwacht falen naar 500.
De response bevat een stabiele publieke code en veilige boodschap, niet err.Error(). Interne fouten kunnen tabelnamen, upstream-URL’s of identifiers bevatten. Een request-correlation-ID verbindt de publieke fout aan de serverlog zonder de chain te tonen.
switch {
case errors.Is(err, ErrNotFound):
writeProblem(w, http.StatusNotFound, "account_not_found")
case errors.Is(err, ErrConflict):
writeProblem(w, http.StatusConflict, "account_conflict")
case errors.Is(err, context.Canceled):
return
default:
logger.ErrorContext(ctx, "update account failed", "error", err)
writeProblem(w, http.StatusInternalServerError, "internal_error")
}Behandel cancellation als control flow
context.Canceled betekent vaak dat de caller vertrok of een parent stopte. context.DeadlineExceeded betekent dat een budget op is. Beide verdienen metrics, want veel deadlines kunnen een slechte dependency tonen, maar het zijn niet altijd applicatiefouten. Iedere client-cancellation als error loggen maakt dashboards vooral luid.
Cancellation werkt alleen wanneer dezelfde context databasequeries, HTTP-requests en goroutines bereikt. Een gewrapte contextfout blijft via errors.Is classificeerbaar. Vervangen door een nieuwe string verliest dat signaal en maakt shutdown lastiger van echt falen te onderscheiden.
Log één keer op de laag die kan handelen
Wanneer repository, service en handler dezelfde fout loggen, wordt één mislukte query drie meldingen zonder extra informatie. Lagere lagen retourneren context. De grens die eigenaar is van request, job of command logt de uiteindelijke onverwachte fout één keer met velden als operatie, request-ID, job-ID en duur.
Verwachte uitkomsten hebben meestal geen errorlog nodig. Een ontbrekend optioneel record of afgewezen validatie past in normale metrics en accesslogs. Bij onverwachte fouten blijft de volledige wrapped chain intern; secrets, credentials, bodies en persoonsgegevens blijven uit de logvelden.
Test het contract en niet de bewoording
Tests controleren errors.Is of errors.As en de zichtbare response aan de grens. Exacte stringvergelijkingen maken nuttige extra context onnodig breaking. Ik test dat wrapped ErrNotFound dezelfde publieke code geeft, een validatiefout velden behoudt en een onbekende dependencyfout nooit in de response verschijnt.
Voor package-API’s documenteer ik welke foutcategorieën callers mogen inspecteren. Al het andere is diagnostische context en geen compatibiliteitsbelofte. Daardoor blijft foutafhandeling bruikbaar wanneer opslag, clients of interne lagen veranderen.
Praktische checklist
- Voeg operatie en onderwerp toe aan errors
- Expose alleen stabiele oorzaken met %w
- Vertaal infrastructuurfouten naar domeinbetekenis
- Gebruik errors.Is en errors.As in plaats van tekstmatching
- Map fouten één keer aan de protocolgrens
- Log onverwachte fouten één keer met veilige context
- Test categorieën en responses in plaats van exacte tekst
