Criando erros customizados com errors.New e fmt.Errorf

1. Introdução aos erros em Go

Em Go, erros são valores. Essa filosofia fundamental é concretizada pela interface nativa error, que possui um único método:

type error interface {
    Error() string
}

Qualquer tipo que implemente o método Error() retornando uma string satisfaz automaticamente essa interface. Isso significa que você pode criar seus próprios tipos de erro com a estrutura que desejar.

Criar erros customizados é importante porque:
- Clareza semântica: um erro como ErrNotFound comunica intenção melhor que uma string solta
- Rastreabilidade: erros com contexto (como IDs ou campos de formulário) facilitam debugging
- Testabilidade: erros sentinela permitem comparação precisa em testes

Go oferece duas abordagens principais para criar erros: errors.New para erros estáticos e fmt.Errorf para erros dinâmicos. Vamos explorar cada uma.

2. Criando erros simples com errors.New

A função errors.New recebe uma string e retorna um valor do tipo error. É a forma mais direta de criar um erro:

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("algo deu errado")
    fmt.Println(err) // algo deu errado
}

Para reutilização, armazene erros em variáveis. Por convenção, use o prefixo Err:

var ErrNotFound = errors.New("recurso não encontrado")
var ErrUnauthorized = errors.New("acesso não autorizado")
var ErrInvalidInput = errors.New("entrada inválida")

func buscarUsuario(id int) error {
    if id <= 0 {
        return ErrInvalidInput
    }
    // lógica de busca...
    return ErrNotFound
}

Boas práticas: declare erros sentinela no nível do pacote, com visibilidade pública apenas quando outros pacotes precisarem comparar com eles.

3. Formatando erros com fmt.Errorf

Enquanto errors.New aceita apenas strings fixas, fmt.Errorf permite incluir valores dinâmicos usando verbos de formatação:

import "fmt"

func buscarPedido(id int) error {
    if id <= 0 {
        return fmt.Errorf("ID de pedido inválido: %d", id)
    }
    // ... lógica
    return fmt.Errorf("pedido %d não encontrado no sistema", id)
}

Verbos comuns:
- %s – strings
- %d – inteiros
- %v – valor default (qualquer tipo)
- %+v – structs com campos nomeados

A diferença prática entre as abordagens: use errors.New para erros que nunca mudam (erros sentinela) e fmt.Errorf quando precisar incluir contexto variável.

4. Erros sentinela vs. erros dinâmicos

Erros sentinela são variáveis globais imutáveis que representam condições específicas:

var ErrSaldoInsuficiente = errors.New("saldo insuficiente")
var ErrContaBloqueada = errors.New("conta bloqueada")

func sacar(conta Conta, valor float64) error {
    if conta.Bloqueada {
        return ErrContaBloqueada
    }
    if conta.Saldo < valor {
        return ErrSaldoInsuficiente
    }
    // processa saque...
    return nil
}

Use erros sentinela quando:
- O erro representa um estado específico e imutável
- Você precisa comparar erros com == ou errors.Is
- A mensagem não precisa de contexto adicional

Erros dinâmicos com fmt.Errorf são ideais quando:
- O erro precisa incluir valores variáveis (IDs, nomes, timestamps)
- Cada ocorrência do erro tem contexto único
- Você não precisa comparar o erro exato, apenas verificar a mensagem

func processarTransacao(id string, valor float64) error {
    if valor <= 0 {
        return fmt.Errorf("valor inválido para transação %s: R$%.2f", id, valor)
    }
    return nil
}

5. Boas práticas na nomeação e organização

Convenção de nomenclatura:
- Prefixo Err para variáveis de erro (ex: ErrTimeout, ErrConnectionFailed)
- Descrição em PascalCase após o prefixo
- Mensagens em letras minúsculas, sem pontuação final

var ErrInvalidEmail = errors.New("email inválido")
var ErrMaxRetriesExceeded = errors.New("número máximo de tentativas excedido")

Organização em pacotes:
- Agrupe erros relacionados em arquivos dedicados (ex: errors.go)
- Para pacotes grandes, crie um subpacote errors ou errs

// pacote: user/errors.go
package user

var (
    ErrNotFound      = errors.New("usuário não encontrado")
    ErrAlreadyExists = errors.New("usuário já existe")
    ErrInvalidAge    = errors.New("idade inválida")
)

Evite mensagens genéricas como "erro interno" ou "algo deu errado". Seja específico: "conexão com banco de dados recusada" é muito mais útil.

6. Comparação: errors.New vs. fmt.Errorf com %w

errors.New gera erros simples que não encapsulam outros erros. Já fmt.Errorf com o verbo %w permite wrapping de erros, criando uma cadeia:

import (
    "errors"
    "fmt"
)

var ErrDatabase = errors.New("erro no banco de dados")

func buscar(id int) error {
    err := queryDatabase(id)
    if err != nil {
        return fmt.Errorf("falha ao buscar registro %d: %w", id, err)
    }
    return nil
}

Com %w, o erro original é preservado na cadeia, permitindo que errors.Is e errors.As atravessem o wrapping:

err := buscar(42)
if errors.Is(err, ErrDatabase) {
    // Isso será verdadeiro mesmo com o wrapping
    fmt.Println("Problema no banco de dados")
}

errors.New não suporta wrapping – você precisaria concatenar strings manualmente, perdendo a capacidade de inspeção com errors.Is.

7. Erros customizados com structs (além do básico)

Para erros ricos em informação, implemente sua própria struct com a interface error:

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validação falhou no campo '%s': %s", e.Field, e.Message)
}

func validarEmail(email string) error {
    if email == "" {
        return &ValidationError{
            Field:   "email",
            Message: "campo obrigatório",
        }
    }
    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Message: "formato inválido",
        }
    }
    return nil
}

Use errors.As para acessar campos extras:

err := validarEmail("invalido")
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Printf("Campo: %s, Mensagem: %s\n", ve.Field, ve.Message)
}

8. Testando erros customizados

Verificando mensagens com Error():

func TestBuscarUsuario(t *testing.T) {
    err := buscarUsuario(0)
    if err.Error() != "entrada inválida" {
        t.Errorf("mensagem inesperada: %s", err.Error())
    }
}

Usando errors.Is para erros sentinela:

func TestBuscarUsuarioNaoEncontrado(t *testing.T) {
    err := buscarUsuario(999)
    if !errors.Is(err, ErrNotFound) {
        t.Errorf("esperava ErrNotFound, obteve %v", err)
    }
}

Armadilhas comuns:
- Comparar strings diretamente (err.Error() == "not found") quebra se a mensagem mudar
- Usar == com erros dinâmicos sempre falha (cada fmt.Errorf cria um novo erro)
- Esquecer de usar ponteiros em structs de erro (implemente Error() com receiver pointer)

Prefira erros sentinela com errors.Is para testes mais robustos e manuteníveis.


Referências