Erros em Go: errors.Is, errors.As, Wrapping e Sentinel Errors
E aí, pessoal!
Em Go, erros são valores. Não existem exceções: você retorna error e quem chama decide o que fazer. Esse modelo é simples, mas exige que a gente saiba criar, propagar e checar erros de forma consistente. Do contrário viram só strings perdidas ou logs que não ajudam em produção.
Neste post vamos ver na prática: sentinel errors, wrapping com %w, errors.Is, errors.As e padrões para APIs e libs que deixam o código mais fácil de debugar e de tratar.
Se preferir ver o assunto em vídeo, confira no no YouTube onde explico erros em Go na prática.
1) Por que erros são valores em Go
Em Go não há try/catch. A regra é: função retorna error, quem chama trata. Isso significa que:
- O tratamento é explícito em cada camada (ou você propaga de forma consciente).
- Erros podem ser comparados e checados sem truques.
- Você pode acrescentar contexto ao erro ao subir (wrapping) sem perder o erro original.
Para isso funcionar bem, entram em jogo: sentinel errors para condições conhecidas, wrapping para contexto e errors.Is / errors.As para checagem. Vamos por partes.
2) Sentinel errors
Sentinel errors são erros definidos como variáveis (em geral no nível do pacote) e usados para representar uma condição específica que o caller pode querer tratar.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package store
import "errors"
var (
ErrNotFound = errors.New("store: resource not found")
ErrConflict = errors.New("store: conflict")
ErrValidation = errors.New("store: validation failed")
)
func (s *Store) GetByID(ctx context.Context, id string) (*Resource, error) {
r, err := s.db.Query(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return r, nil
}
Quem usa a API pode decidir com base no sentinel:
1
2
3
4
5
6
7
resource, err := store.GetByID(ctx, id)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return nil, nil // ou 404
}
return nil, err
}
Quando usar: para condições de domínio ou contrato que o caller precisa distinguir (ex.: “não encontrado”, “conflito”, “não autorizado”). Evite criar sentinel para cada mensagem. Use só quando o fluxo ou a resposta da API mudar de fato.
3) Wrapping com %w
Wrapping é acrescentar contexto ao erro ao propagá-lo, mantendo o erro original na cadeia. Em Go isso é feito com fmt.Errorf e o verb %w (a partir de Go 1.13).
1
2
3
4
5
6
7
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return user, nil
}
Assim o caller continua podendo usar errors.Is e errors.As no erro retornado, porque o err original fica na cadeia. Use %w uma vez por nível: não faça wrap de novo do mesmo erro no mesmo pacote sem acrescentar contexto novo.
Errado: perder o erro original (não use %v quando quiser inspeção):
1
return nil, fmt.Errorf("get user: %v", err) // errors.Is(err, ErrNotFound) não funciona
Certo: preservar com %w:
1
return nil, fmt.Errorf("get user %s: %w", id, err) // cadeia intacta
4) errors.Is
errors.Is(err, target) verifica se err é exatamente target ou se, em algum ponto da cadeia de wraps, o erro chega a target. É a forma recomendada de checar sentinels.
1
2
3
4
5
6
7
8
9
10
resource, err := store.GetByID(ctx, id)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return nil, nil
}
if errors.Is(err, store.ErrConflict) {
return nil, ErrConflictResponse
}
return nil, err
}
Funciona com erros wrapped:
1
2
3
4
// em algum lugar: return nil, fmt.Errorf("loading resource: %w", store.ErrNotFound)
if errors.Is(err, store.ErrNotFound) {
// ainda é true
}
Use errors.Is sempre que quiser comparar com um valor conhecido (sentinel). Não use err == store.ErrNotFound quando houver wrapping: pode falhar.
5) errors.As
errors.As(err, &target) percorre a cadeia de erros e, se encontrar um erro que implemente o tipo de target, atribui a target e retorna true. Serve para erros que carregam dados (estruturas, códigos, campos).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
}
func ValidateUser(u *User) error {
if u.Email == "" {
return &ValidationError{Field: "email", Message: "required"}
}
return nil
}
Quem chama pode extrair o tipo e usar os campos:
1
2
3
4
5
6
7
if err := ValidateUser(user); err != nil {
var verr *ValidationError
if errors.As(err, &verr) {
return fmt.Sprintf("campo %s: %s", verr.Field, verr.Message)
}
return err.Error()
}
Quando usar: quando o erro precisa carregar informação estruturada (campo inválido, código de negócio). Para condições simples e sem dados extras, sentinel + errors.Is resolve.
6) Padrões para APIs e bibliotecas
Em bibliotecas (baixo nível)
- Retorne sentinels para condições que o caller deve distinguir (ex.:
ErrNotFound). - Não faça wrap dos erros que você mesmo retorna. Deixe o caller adicionar contexto se quiser.
- Para erros de dependências (ex.:
sql.ErrNoRows), traduza para os seus sentinels quando fizer sentido (ex.:ErrNotFound) e retorne direto, sem wrap desnecessário.
1
2
3
4
5
// Lib: retorno direto
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
Em camadas de aplicação (serviços, handlers)
- Faça wrap ao propagar, com contexto útil:
fmt.Errorf("get user %s: %w", id, err). - Defina sentinels no domínio (ex.:
ErrUserNotFound,ErrDuplicateEmail) e useerrors.Isnos handlers para decidir status HTTP ou resposta.
Tipos customizados vs sentinels
| Situação | Recomendação |
|---|---|
| Condição conhecida, sem dados | Sentinel + Is |
| Erro com dados (campo, código) | Tipo customizado + As |
| Só mensagem ao caller | fmt.Errorf com %w |
7) Exemplo completo: API com repositório
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
)
// Pacote domain ou store
var ErrNotFound = errors.New("resource not found")
type User struct{ ID, Email string } // simplificado para o exemplo
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string { return e.Field + ": " + e.Msg }
// Repositório: retorna sentinels, sem wrap
func (r *Repo) FindByID(ctx context.Context, id string) (*User, error) {
var u User
err := r.db.GetContext(ctx, &u, "SELECT * FROM users WHERE id = $1", id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &u, nil
}
// Serviço: wrap ao propagar
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return user, nil
}
// Handler HTTP: Is e As para resposta
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.svc.GetUser(r.Context(), r.PathValue("id"))
if err != nil {
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
var valErr *ValidationError
if errors.As(err, &valErr) {
http.Error(w, valErr.Msg, http.StatusBadRequest)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
8) O que evitar
- Não use
err == sentinelquando houver wrapping. Prefiraerrors.Is(err, sentinel). - Não use
%vpara o erro interno se quiser que o caller useIs/As. Use%w. - Não crie sentinels demais. Reserve para condições que o caller realmente trata de forma diferente.
- Não coloque dados voláteis ou sensíveis em tipos de erro que podem ser logados (evite senhas, tokens).
Conclusão
Um bom modelo de erros em Go combina:
- Sentinel errors para condições que o caller precisa identificar (
errors.Is). - Wrapping com
%wnas camadas de aplicação para dar contexto sem perder a cadeia. - Tipos customizados quando o erro precisa carregar dados (
errors.As). - Regras claras em APIs e libs: sentinels onde fizer sentido, wrap na aplicação, sem exagero na mesma camada.
Com isso os erros ficam mais fáceis de logar e de tratar em handlers e clientes. Para aprofundar, o post Por que o context.Context é o sistema nervoso do Go moderno mostra como contexto e cancelamento se integram com fluxos que retornam erro.
Até a próxima!
Referências
- Package errors – Documentação oficial do pacote
errors(errors.Is, errors.As, wrapping). - Working with Errors in Go 1.13 – Post do Go Blog sobre error wrapping e
%w(Go 1.13). - Effective Go, seção Errors – Tratamento de erros no guia oficial.
- Errors are values – Post do Go Blog (Rob Pike) sobre erros como valores.
- Don’t just check errors, handle them gracefully – Dave Cheney sobre sentinel errors, wrapping e boas práticas.
- Go Code Review Comments, Error strings – Convenções para mensagens e tratamento de erros.
