KoderKoder.ai
PrezziEnterpriseIstruzionePer gli investitori
AccediInizia ora

Prodotto

PrezziEnterprisePer gli investitori

Risorse

ContattaciAssistenzaIstruzioneBlog

Note legali

Informativa sulla privacyTermini di utilizzoSicurezzaNorme di utilizzoSegnala un abuso

Social

LinkedInTwitter
Koder.ai
Lingua

© 2026 Koder.ai. Tutti i diritti riservati.

Home›Blog›Pattern per la gestione degli errori delle API Go per risposte chiare e coerenti
29 set 2025·7 min

Pattern per la gestione degli errori delle API Go per risposte chiare e coerenti

Pattern per la gestione degli errori nelle API Go che standardizzano errori tipizzati, codici di stato HTTP, ID di richiesta e messaggi sicuri senza esporre dettagli interni.

Pattern per la gestione degli errori delle API Go per risposte chiare e coerenti

Perché errori API incoerenti infastidiscono i client

Quando ogni endpoint segnala i fallimenti in modo diverso, i client smettono di fidarsi della tua API. Una rotta restituisce { "error": "not found" }, un'altra { "message": "missing" } e una terza invia testo semplice. Anche se il significato è simile, il codice client deve ora indovinare cosa è successo.

Il costo si manifesta rapidamente. I team scrivono logiche di parsing fragili e aggiungono casi speciali per ogni endpoint. I retry diventano rischiosi perché il client non riesce a distinguere “prova più tardi” da “il tuo input è sbagliato.” I ticket di supporto aumentano perché il client vede solo un messaggio vago e il tuo team non riesce facilmente a ricondurlo a una riga di log server.

Uno scenario comune: un'app mobile chiama tre endpoint durante la registrazione. Il primo restituisce HTTP 400 con una mappa di errori per campo, il secondo restituisce HTTP 500 con una stringa di stack trace e il terzo restituisce HTTP 200 con { "ok": false }. Il team dell'app spedisce tre handler diversi e il team backend continua a ricevere segnalazioni tipo “la registrazione a volte fallisce” senza indizi su dove iniziare.

L'obiettivo è un contratto prevedibile. I client dovrebbero poter leggere in modo affidabile cosa è successo: se è colpa loro o del server, se ha senso ritentare, e un ID di richiesta da incollare al supporto.

Nota di ambito: questo si concentra su API HTTP JSON (non gRPC), ma le stesse idee valgono ovunque tu ritorni errori ad altri sistemi.

Un obiettivo semplice: un solo contratto seguito da tutti gli endpoint

Scegli un contratto chiaro per gli errori e applicalo a ogni endpoint. “Coerente” significa la stessa forma JSON, lo stesso significato dei campi e lo stesso comportamento indipendentemente da quale handler fallisca. Una volta fatto ciò, i client smettono di indovinare e cominciano a gestire gli errori.

Un contratto utile aiuta i client a decidere cosa fare dopo. Per la maggior parte delle app, ogni risposta di errore dovrebbe rispondere a tre domande:

  • Posso correggere il mio input?
  • Devo ritentare più tardi?
  • Devo contattare il supporto?

Un set pratico di regole:

  • Uno schema di risposta per tutti gli errori.
  • Una sola policy di codici di stato (lo stesso tipo di errore mappa sempre allo stesso HTTP status).
  • Una sola policy per messaggi sicuri (cosa può vedere l'utente vs cosa resta interno).
  • Un aggancio di correlazione (un request ID restituito così il supporto può ritrovare il fallimento).

Decidi in anticipo cosa non deve mai apparire nelle risposte. Gli elementi comuni da escludere includono frammenti SQL, stack trace, nomi host interni, segreti e stringhe di errore grezze da dipendenze.

Mantieni una separazione pulita: un messaggio breve rivolto all'utente (sicuro, cortese, azionabile) e dettagli interni (errore completo, stack e contesto) conservati nei log. Per esempio, “Impossibile salvare le modifiche. Riprova.” è sicuro. “pq: duplicate key value violates unique constraint users_email_key” non lo è.

Quando ogni endpoint segue lo stesso contratto, i client possono costruire un unico handler di errori e riutilizzarlo ovunque.

Definisci uno schema di risposta di errore su cui i client possono fare affidamento

I client possono gestire gli errori solo se ogni endpoint risponde con la stessa forma. Scegli un involucro JSON e mantienilo stabile.

Un default pratico è un oggetto error più un request_id a livello top:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Some fields are invalid.",
    "details": {
      "fields": {
        "email": "must be a valid email address"
      }
    }
  },
  "request_id": "req_01HV..."
}

Lo status HTTP dà la categoria ampia (400, 401, 409, 500). Il error.code leggibile dalla macchina dà il caso specifico su cui il client può fare branching. Questa separazione è importante perché molti problemi diversi condividono lo stesso status. Un'app mobile può mostrare UI diversa per EMAIL_TAKEN vs WEAK_PASSWORD, anche se entrambi sono 400.

Mantieni error.message sicuro e leggibile. Deve aiutare l'utente a risolvere il problema, ma non deve mai rivelare interni (SQL, stack trace, nomi di provider, percorsi di file).

I campi opzionali sono utili quando restano prevedibili:

  • Errori di validazione: details.fields come mappa campo → messaggio.
  • Limiti di rate o problemi temporanei: details.retry_after_seconds.
  • Guida extra: details.docs_hint come testo semplice (non un URL).

Per compatibilità retroattiva, tratta i valori di error.code come parte del contratto API. Aggiungi nuovi codici senza cambiare i significati vecchi. Aggiungi solo campi opzionali e supponi che i client ignorino campi non riconosciuti.

Errori tipizzati in Go: un modello pulito per gli handler

La gestione degli errori diventa disordinata quando ogni handler inventa il proprio modo di segnalare il fallimento. Un piccolo set di errori tipizzati risolve il problema: gli handler restituiscono tipi di errore noti e uno strato di risposta li trasforma in risposte coerenti.

Un set pratico di partenza copre la maggior parte degli endpoint:

  • ValidationError (input non valido)
  • NotFoundError (risorsa mancante)
  • ConflictError (vincolo unico, mismatch di stato)
  • UnauthorizedError (non autenticato o non autorizzato)
  • InternalError (tutto il resto)

La chiave è la stabilità a livello superiore, anche se la causa radice cambia. Puoi avvolgere errori di basso livello (SQL, rete, parsing JSON) restituendo lo stesso tipo pubblico che il middleware può rilevare.

type NotFoundError struct {
	Resource string
	ID       string
	Err      error // private cause
}

func (e NotFoundError) Error() string { return "not found" }
func (e NotFoundError) Unwrap() error { return e.Err }

Nel tuo handler, restituisci NotFoundError{Resource: "user", ID: id, Err: err} invece di far trapelare direttamente sql.ErrNoRows.

Per verificare gli errori, preferisci errors.As per i tipi custom e errors.Is per errori sentinel. Gli errori sentinel (come var ErrUnauthorized = errors.New("unauthorized")) funzionano per casi semplici, ma i tipi custom vincono quando hai bisogno di contesto sicuro (per esempio quale risorsa mancava) senza cambiare il tuo contratto pubblico.

Sii rigoroso su cosa alleghi:

  • Pubblico (sicuro per i client): un messaggio breve, un codice stabile e talvolta il nome del campo per la validazione.
  • Privato (solo log): la Err sottostante, informazioni sullo stack, errori SQL grezzi, token, dati utente.

Questa separazione ti permette di aiutare i client senza esporre gli interni.

Mappa i tipi di errore ai codici di stato HTTP in modo coerente

Una volta che hai errori tipizzati, il passo successivo è noioso ma essenziale: lo stesso tipo di errore dovrebbe sempre produrre lo stesso status HTTP. I client costruiranno logica attorno a questo.

Una mappatura pratica che funziona per la maggior parte delle API:

Error type (example)StatusWhen to use it
BadRequest (malformed JSON, missing required query param)400The request is not valid at a basic protocol or format level.
Unauthenticated (no/invalid token)401The client needs to authenticate.
Forbidden (no permission)403Auth is valid, but access is not allowed.
NotFound (resource ID does not exist)404The requested resource is not there (or you choose to hide existence).
Conflict (unique constraint, version mismatch)409The request is well-formed, but it clashes with current state.
ValidationFailed (field rules)422The shape is fine, but business validation fails (email format, min length).
RateLimited429Too many requests in a time window.
Internal (unknown error)500Bug or unexpected failure.
Unavailable (dependency down, timeout, maintenance)503Temporary server-side issue.

Due distinzioni che evitano molta confusione:

  • 400 vs 422: usa 400 quando non puoi interpretare in modo affidabile la richiesta (JSON malformato, tipi sbagliati). Usa 422 quando puoi parsearla, ma i valori non sono accettabili.
  • 409 vs 422: usa 422 per validazione a livello di campo (password troppo corta). Usa 409 quando i dati sono validi ma non applicabili a causa dello stato (email già presa, ordine già spedito, failure di lock ottimistico).

Indicazioni sul retry:

  • Solitamente sicuro ritentare: 503, e talvolta 429 (dopo aver aspettato).
  • Solitamente non sicuro ritentare senza modifiche: 400, 401, 403, 404, 409, 422.
  • Se l'operazione è idempotente (PUT con lo stesso body, o POST con una chiave di idempotenza), i retry diventano più sicuri anche dopo errori transitori.

Request ID: il modo più rapido per debuggare problemi client

Plan your error contract
Draft a consistent JSON error contract and request ID policy in Koder.ai planning mode.
Try Free

Un request ID è un valore unico breve che identifica una chiamata API end-to-end. Se i client lo vedono in ogni risposta, il supporto diventa semplice: “Mandami il request ID” è spesso sufficiente per trovare i log esatti e il fallimento esatto.

Questa abitudine ripaga sia per risposte di successo che di errore.

Regole di generazione e propagazione

Usa una regola chiara: se il client manda un request ID, conservalo. Altrimenti, creane uno.

  • Accetta un ID in ingresso da un singolo header (scegline uno e documentalo, per esempio X-Request-Id).
  • Se l'header manca o è vuoto, genera un nuovo ID al bordo (middleware) e allegalo al context della richiesta.
  • Non cambiare mai l'ID a metà richiesta. Passalo alle chiamate downstream (DB, altri servizi) via context o header.

Metti il request ID in tre posti:

  • Header di risposta (stesso nome che accetti)
  • Body della risposta (come request_id nello schema standard)
  • Log (come campo strutturato su ogni riga di log)

Lavori batch e asincroni

Per endpoint batch o job in background, conserva un request ID padre. Esempio: un client carica 200 righe, 12 falliscono la validazione e tu metti in coda il lavoro. Restituisci un solo request_id per tutta la chiamata e includi un parent_request_id su ogni job e su ogni errore per item. Così puoi tracciare “un upload” anche quando si espande in molti task.

Logging e metriche senza divulgare interni

I client hanno bisogno di una risposta di errore chiara e stabile. I tuoi log hanno bisogno della verità incasinata. Mantieni separati questi due mondi: ritorna un messaggio pubblico sicuro e un codice errore, mentre logghi la causa interna, lo stack e il contesto sul server.

Registra un evento strutturato per ogni risposta di errore, ricercabile tramite request_id.

Campi che vale la pena mantenere coerenti:

  • request_id
  • user_id o account_id (quando autenticato)
  • codice errore pubblico e status HTTP
  • nome handler/route e metodo
  • dettaglio errore interno (causa wrappata, errori di validazione per campo, timeout upstream)

Conserva i dettagli interni solo nei log server (o in uno store di errori interno). Il client non dovrebbe mai vedere errori database grezzi, query, stack trace o messaggi dei provider. Se hai più servizi, un campo interno come source (api, db, auth, upstream) può velocizzare il triage.

Controlla endpoint rumorosi e errori rate-limited. Se un endpoint può produrre lo stesso 429 o 400 migliaia di volte al minuto, evita lo spam nei log: campiona eventi ripetuti o abbassa la severità per errori attesi continuando però a contarli nelle metriche.

Le metriche catturano i problemi prima dei log. Traccia conteggi raggruppati per status HTTP e codice errore, e alert su picchi improvvisi. Se RATE_LIMITED sale di 10x dopo un deploy, lo vedrai rapidamente anche se i log sono campionati.

Passo dopo passo: implementare una pipeline di errori coerente in Go

Get rewarded for building
Share what you built with Koder.ai and get credits to keep building.
Earn Credits

Il modo più semplice per rendere gli errori coerenti è smettere di gestirli “ovunque” e instradarli attraverso una piccola pipeline. Quella pipeline decide cosa vede il client e cosa conservi per i log.

La pipeline in 5 passi pratici

Inizia con un piccolo set di codici di errore su cui i client possono contare (per esempio: INVALID_ARGUMENT, NOT_FOUND, UNAUTHORIZED, CONFLICT, INTERNAL). Avvolgili in un errore tipizzato che espone solo campi pubblici e sicuri (code, messaggio sicuro, dettagli opzionali come quale campo è sbagliato). Mantieni le cause interne private.

Poi implementa una funzione traduttrice che converte qualsiasi errore in (statusCode, responseBody). Qui i tipi tipizzati mappano agli status HTTP e gli errori sconosciuti diventano una sicura risposta 500.

Aggiungi poi middleware che:

  • garantisca che ogni richiesta abbia un request_id
  • recuperi dai panic

Un panic non dovrebbe mai scaricare stack trace al client. Restituisci un normale 500 con un messaggio generico e logga il panic completo con lo stesso request_id.

Infine, modifica i tuoi handler in modo che restituiscano un error invece di scrivere direttamente la risposta. Un wrapper può chiamare l'handler, eseguire il traduttore e scrivere il JSON nello standard.

Una checklist compatta:

  • Definisci errori tipizzati con campi sicuri e codici stabili.
  • Traduce errori in status e JSON di risposta in un unico punto.
  • Aggiungi middleware per request ID e recovery da panic.
  • Fai restituire errori agli handler, non risposte dirette.
  • Aggiungi test golden per il traduttore e il wrapper.

I test golden sono importanti perché bloccano il contratto. Se qualcuno cambia un messaggio o un codice di stato, i test falliscono prima che i client vengano sorpresi.

Esempio: un endpoint, tre fallimenti, risposte prevedibili

Immagina un endpoint: un'app client crea un record cliente.

POST /v1/customers con JSON come { "email": "[email protected]", "name": "Pat" }. Il server restituisce sempre la stessa forma di errore e include sempre un request_id.

1) Errore di validazione (400)

L'email manca o è malformata. Il client può evidenziare il campo.

{
  "request_id": "req_01HV9N2K6Q7A3W1J9K8B",
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Some fields need attention.",
    "details": {
      "fields": {
        "email": "must be a valid email address"
      }
    }
  }
}

2) Conflict (409)

L'email esiste già. Il client può suggerire di accedere o sceglierne un'altra.

{
  "request_id": "req_01HV9N3C2D0F0M3Q7Z9R",
  "error": {
    "code": "ALREADY_EXISTS",
    "message": "A customer with this email already exists."
  }
}

3) Fallimento transitorio (503)

Una dipendenza è giù. Il client può ritentare con backoff e mostrare un messaggio calmo.

{
  "request_id": "req_01HV9N3X8P2J7T4N6C1D",
  "error": {
    "code": "TEMPORARILY_UNAVAILABLE",
    "message": "We could not save your request right now. Please try again."
  }
}

Con un solo contratto, il client reagisce in modo coerente:

  • 400: marca i campi usando details.fields
  • 409: guida l'utente a un passo successivo sicuro
  • 503: suggerisce il retry e mostra request_id come ID per il supporto

Per il supporto, lo stesso request_id è il percorso più rapido per trovare la causa reale nei log interni, senza esporre stack trace o errori di database.

Trappole comuni che peggiorano la gestione degli errori

Il modo più rapido per infastidire i client API è farli indovinare. Se un endpoint ritorna { "error": "..." } e un altro { "message": "..." }, ogni client si trasforma in una catasta di casi speciali e i bug restano nascosti per settimane.

Alcuni errori ricorrenti:

  • Restituire HTTP 200 con un errore nel body, o alternare più schemi di errore tra endpoint.
  • Esporre internals nel messaggio utente, come errori SQL, stack trace, IP, host di dipendenze o percorsi di file.
  • Usare testo umano come unico identificatore, invece di un code stabile su cui i client possano affidarsi.
  • Cambiare i codici di errore in modo casuale (o riusare lo stesso codice per problemi diversi), rompendo i client scritti per il comportamento precedente.
  • Aggiungere request_id solo sui fallimenti, così non puoi correlare una segnalazione utente con la chiamata di successo che ha innescato il problema successivo.

Far trapelare gli interni è la trappola più facile. Un handler restituisce err.Error() perché è comodo, e poi un nome di vincolo o un messaggio di terze parti finisce in produzione. Mantieni il messaggio client breve e sicuro e metti la causa dettagliata nei log.

Affidarsi solo al testo è un altro problema lento. Se il client deve parsare frasi in inglese come “email already exists”, non puoi cambiare la formulazione senza rompere la logica. I codici di errore stabili ti permettono di aggiustare i messaggi, tradurli e mantenere il comportamento coerente.

Tratta i codici di errore come parte del contratto pubblico. Se devi cambiarne uno, aggiungi un nuovo codice e mantieni quello vecchio funzionante per un po’, anche se entrambi mappano allo stesso status HTTP.

Infine, includi lo stesso campo request_id in ogni risposta, successo o fallimento. Quando un utente dice “prima funzionava, poi ha smesso”, quell'ID spesso salva ore di indagini.

Checklist rapida prima del rilascio

Centralize error handling
Turn your typed error model into a shared mapper layer across endpoints.
Create Backend

Prima del deploy, fai un controllo rapido per la coerenza:

  • Una forma di errore ovunque. Ogni endpoint restituisce gli stessi campi JSON (per esempio: error.code, error.message, request_id).
  • Codici di errore stabili con copertura. Mantieni i codici brevi e semplici (VALIDATION_FAILED, NOT_FOUND, CONFLICT, UNAUTHORIZED). Aggiungi test così gli handler non possono restituire codici sconosciuti per errore.
  • Una regola unica per la mappatura agli status. Decidi come ogni tipo di errore mappa a uno status HTTP e applicala in un posto condiviso.
  • Request ID in entrambe le direzioni. Restituisci sempre un request_id e registralo per ogni richiesta, inclusi panic e timeout.
  • Messaggi sicuri per default. I messaggi rivolti all'utente dovrebbero essere brevi, chiari e azionabili; mai stack trace, errori SQL o nomi di vendor.

Dopo di ciò, verifica manualmente alcuni endpoint. Genera un errore di validazione, una risorsa mancante e un errore inaspettato. Se le risposte sono diverse tra endpoint (campi che cambiano, status che divergono, messaggi che sovraespongono), correggi la pipeline condivisa prima di aggiungere altre funzionalità.

Una regola pratica: se un messaggio aiuterebbe un attaccante o confonderebbe un utente normale, appartiene ai log, non alla risposta.

Prossimi passi: standardizza ora e mantieni la coerenza in seguito

Scrivi il contratto di errore che vuoi che tutti gli endpoint seguano, anche se la tua API è già live. Un contratto condiviso (status, codice errore stabile, messaggio sicuro e request_id) è il modo più rapido per rendere gli errori prevedibili per i client.

Poi migra gradualmente. Mantieni gli handler esistenti, ma instrada i loro fallimenti attraverso un mapper che converte gli errori interni nella forma pubblica. Questo migliora la coerenza senza una riscrittura rischiosa e impedisce ai nuovi endpoint di inventare nuovi formati.

Tieni un piccolo catalogo di codici di errore e trattalo come parte della tua API. Quando qualcuno vuole aggiungerne uno nuovo, fai una rapida revisione: è veramente nuovo, è nominato chiaramente e mappa al corretto status HTTP?

Aggiungi alcuni test che catturino la deriva:

  • Ogni risposta di errore include request_id.
  • Lo status code corrisponde al tipo di errore (non al testo dell'errore).
  • error.code è presente e proviene dal catalogo.
  • error.message resta sicuro e non include dettagli interni.
  • Errori sconosciuti ricadono su 500 con un messaggio generico.

Se stai costruendo un backend Go da zero, può aiutare bloccare il contratto presto. Per esempio, Koder.ai (koder.ai) include una modalità di pianificazione dove puoi definire convenzioni come uno schema di errore e un catalogo di codici fin dall'inizio, quindi mantenere gli handler allineati man mano che l'API cresce.

Domande frequenti

What should a “consistent error response” look like?

Usa una sola forma JSON per tutte le risposte di errore, in tutti gli endpoint. Un'impostazione pratica è un request_id in cima e un oggetto error con code, message e opzionali details, così i client possono parsare e reagire in modo affidabile.

How do I avoid leaking internal details in API errors?

Restituisci error.message come una frase breve e sicura per l'utente e conserva la causa reale nei log del server. Non ritornare errori grezzi del database, stack trace, host interni o messaggi di dipendenze, anche se possono aiutare in sviluppo.

Do I really need an error code if I already have HTTP status codes?

Usa un error.code stabile per la logica macchina e lascia che lo status HTTP descriva la categoria ampia. I client dovrebbero fare branching su error.code (ad es. ALREADY_EXISTS) e usare lo status come guida (ad es. 409 indica conflitto di stato).

When should I use HTTP 400 vs 422?

Usa 400 quando la richiesta non può essere interpretata in modo affidabile (JSON malformato, tipi sbagliati). Usa 422 quando la richiesta è ben formata ma viola regole di business (formato email non valido, password troppo corta).

When should I use HTTP 409 vs 422?

Usa 409 quando l'input è valido ma non può essere applicato a causa dello stato corrente (email già usata, conflitto di versione). Usa 422 per validazioni a livello di campo dove cambiare il valore risolve il problema senza dipendere dallo stato del server.

How do typed errors in Go help keep responses consistent?

Crea un piccolo insieme di errori tipizzati (validation, not found, conflict, unauthorized, internal) e fai sì che gli handler li restituiscano. Poi usa un traduttore condiviso per mappare quei tipi in status HTTP e nella forma JSON standard.

How should I generate and return request IDs?

Restituisci sempre un request_id in ogni risposta, sia di successo sia di errore, e registralo in ogni riga di log del server. Se un client segnala un problema, quell'ID dovrebbe essere sufficiente per trovare il percorso esatto del fallimento nei log.

Why is returning HTTP 200 with `{ "ok": false }` a bad idea?

Restituisci 200 solo quando l'operazione è riuscita, e usa 4xx/5xx per gli errori. Nascondere errori dietro 200 obbliga i client a parsare campi nel body e crea comportamenti incoerenti tra endpoint.

Which errors should clients retry, and which should they not?

Di norma non riprovare per 400, 401, 403, 404, 409 e 422 perché il retry non aiuta senza modifiche. Permetti il retry per 503 e talvolta per 429 dopo aver aspettato; se supporti chiavi di idempotenza, i retry diventano più sicuri per POST su errori transitori.

How do I prevent error responses from drifting as the API evolves?

Blocca il contratto con alcuni test “golden” che asseriscano status, error.code e presenza di request_id. Aggiungi nuovi codici senza cambiare i significati vecchi e aggiungi solo campi opzionali così i client più vecchi continuano a funzionare.

Indice
Perché errori API incoerenti infastidiscono i clientUn obiettivo semplice: un solo contratto seguito da tutti gli endpointDefinisci uno schema di risposta di errore su cui i client possono fare affidamentoErrori tipizzati in Go: un modello pulito per gli handlerMappa i tipi di errore ai codici di stato HTTP in modo coerenteRequest ID: il modo più rapido per debuggare problemi clientLogging e metriche senza divulgare interniPasso dopo passo: implementare una pipeline di errori coerente in GoEsempio: un endpoint, tre fallimenti, risposte prevedibiliTrappole comuni che peggiorano la gestione degli erroriChecklist rapida prima del rilascioProssimi passi: standardizza ora e mantieni la coerenza in seguitoDomande frequenti
Condividi
Koder.ai
Build your own app with Koder today!

The best way to understand the power of Koder is to see it for yourself.

Start FreeBook a Demo