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