Wrapping e unwrapping de erros com errors.Is e errors.As
1. O problema de comparar erros encadeados
Em Go, erros são valores. Tradicionalmente, comparamos erros com o operador ==. Esse padrão funciona bem quando o erro retornado é exatamente o mesmo valor definido como sentinel error. No entanto, à medida que a aplicação cresce e os erros são propagados por várias camadas, esse operador se torna insuficiente.
Considere o cenário onde uma função de baixo nível retorna um erro sentinela:
var ErrNotFound = errors.New("recurso não encontrado")
func buscarNoBanco(id int) error {
// simulação de erro
return ErrNotFound
}
func buscarUsuario(id int) error {
err := buscarNoBanco(id)
if err != nil {
return fmt.Errorf("falha ao buscar usuário: %v", err)
}
return nil
}
func main() {
err := buscarUsuario(42)
fmt.Println(err == ErrNotFound) // false! O erro foi "perdido"
}
O problema é claro: ao usar %v ou %s, o erro original é convertido em string e perdemos a identidade do erro. A pilha de chamadas fica opaca, impossibilitando decisões baseadas no tipo ou valor específico do erro.
2. Wrapping de erros com fmt.Errorf e %w
O Go 1.13 introduziu o verbo %w no fmt.Errorf, permitindo encapsular (wrap) um erro dentro de outro:
func buscarUsuario(id int) error {
err := buscarNoBanco(id)
if err != nil {
return fmt.Errorf("falha ao buscar usuário: %w", err)
}
return nil
}
A diferença crucial entre %w e %v/%s é que %w preserva o erro original na cadeia de wrapping. O erro resultante pode ser "desembrulhado" posteriormente.
Limitação importante: você só pode usar um %w por chamada de fmt.Errorf. Para múltiplos erros, é necessário usar técnicas como errors.Join (Go 1.20+) ou criar tipos customizados:
// Go 1.20+
err := errors.Join(
fmt.Errorf("erro de conexão: %w", ErrConnection),
fmt.Errorf("timeout: %w", ErrTimeout),
)
3. errors.Is: verificando a árvore de erros
A função errors.Is percorre recursivamente a cadeia de erros, desempacotando cada camada até encontrar uma correspondência exata:
func main() {
err := buscarUsuario(42)
if errors.Is(err, ErrNotFound) {
fmt.Println("Erro de recurso não encontrado detectado!")
}
}
Internamente, errors.Is funciona assim:
1. Verifica se o erro atual é igual ao alvo (==)
2. Se o erro implementa Is(error) bool, delega a decisão
3. Se o erro implementa Unwrap() error, chama recursivamente
4. Se implementa Unwrap() []error (Go 1.20+), verifica todos
Exemplo prático: validação de permissão em múltiplas camadas:
var (
ErrPermissionDenied = errors.New("permissão negada")
ErrAccessDenied = errors.New("acesso negado")
)
func verificarPermissao(usuario string) error {
if usuario != "admin" {
return fmt.Errorf("usuário %s: %w", usuario, ErrPermissionDenied)
}
return nil
}
func acessarArquivo(usuario string) error {
if err := verificarPermissao(usuario); err != nil {
return fmt.Errorf("falha no acesso: %w", err)
}
return nil
}
func main() {
err := acessarArquivo("guest")
if errors.Is(err, ErrPermissionDenied) {
fmt.Println("Permissão insuficiente para acessar o arquivo")
}
}
4. errors.As: extraindo tipos específicos da cadeia
Enquanto errors.Is verifica valores, errors.As verifica tipos. É a ferramenta ideal quando você precisa acessar dados específicos de um erro encapsulado:
func abrirArquivo(nome string) error {
_, err := os.Open(nome)
if err != nil {
return fmt.Errorf("erro ao abrir %s: %w", nome, err)
}
return nil
}
func main() {
err := abrirArquivo("/tmp/inexistente.txt")
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Operação: %s\n", pathErr.Op)
fmt.Printf("Caminho: %s\n", pathErr.Path)
fmt.Printf("Erro subjacente: %v\n", pathErr.Err)
}
}
Diferenças importantes:
- errors.Is → compara valores (útil para sentinel errors)
- errors.As → extrai tipos (útil para erros com estrutura interna)
O segundo argumento de errors.As deve ser um ponteiro duplo (**T) ou uma interface (*interface{}). Isso permite que a função modifique o ponteiro para apontar para o erro encontrado.
5. Implementando a interface Unwrap() em erros customizados
Para que seus tipos de erro personalizados participem da cadeia de wrapping, implemente a interface Unwrap() error:
type ValidationError struct {
Field string
Message string
Err error // erro original encapsulado
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validação falhou em %s: %s", e.Field, e.Message)
}
func (e *ValidationError) Unwrap() error {
return e.Err
}
func validarCampo(valor string) error {
if valor == "" {
return &ValidationError{
Field: "nome",
Message: "campo obrigatório",
Err: ErrInvalidInput,
}
}
return nil
}
func processarFormulario() error {
if err := validarCampo(""); err != nil {
return fmt.Errorf("formulário inválido: %w", err)
}
return nil
}
func main() {
err := processarFormulario()
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Campo problemático: %s\n", valErr.Field)
}
if errors.Is(err, ErrInvalidInput) {
fmt.Println("Entrada inválida detectada na cadeia")
}
}
6. Boas práticas e armadilhas comuns
Não usar %w para erros que não devem ser comparados:
// RUIM: erro temporário não deve ser comparável
return fmt.Errorf("serviço indisponível: %w", ErrTemporary)
// MELHOR: usar %v para erros que são apenas informativos
return fmt.Errorf("serviço indisponível: %v", ErrTemporary)
Cuidado com loops infinitos em Unwrap():
// PERIGOSO: Unwrap retornando a si mesmo causa loop infinito
func (e *MyError) Unwrap() error {
return e // NUNCA faça isso!
}
Quando preferir errors.Is/errors.As vs switch com type assertion:
Use errors.Is/errors.As quando o erro pode estar encapsulado em múltiplas camadas. Use type assertion direta apenas quando você tem certeza absoluta do tipo exato do erro retornado.
Wrapping vs logging:
- Wrapping: preserve o erro para tomada de decisão em camadas superiores
- Logging: registre o erro em pontos estratégicos, mas não em toda camada
// Bom: wrap + log no ponto de decisão
func servico() error {
err := camadaInterna()
if err != nil {
log.Printf("serviço falhou: %v", err)
return fmt.Errorf("serviço: %w", err)
}
return nil
}
// Ruim: log em toda camada polui os logs
func camada1() error {
err := camada2()
log.Printf("camada1: %v", err) // redundante
return err
}
Dominar errors.Is e errors.As é fundamental para escrever código Go robusto e manutenível. Essas ferramentas permitem criar hierarquias de erros ricas em informação sem perder a capacidade de identificar e reagir a condições específicas.
Referências
- Tutorial: Errors in Go (Documentação Oficial) — Artigo oficial do Go blog introduzindo wrapping de erros, errors.Is e errors.As
- Package errors (pkg.go.dev) — Documentação completa do pacote errors da biblioteca padrão
- Working with Errors in Go 1.13 (The Go Blog) — Dave Cheney explica a filosofia de erros como valores e as novas funcionalidades
- Error Handling in Go: Best Practices (Medium/TechBlog) — Guia prático com exemplos de wrapping e unwrapping em aplicações reais
- Go Errors: From Legacy to Modern Patterns (Alex Edwards) — Tutorial abrangente cobrindo desde erros básicos até patterns avançados com errors.Is/errors.As