Menu

Tratamento de erros em Golang: if err != nil, empacotar e verificar

Go trata erros como valores comuns devolvidos por funções. Veja a interface error, if err != nil, errors.New e fmt.Errorf, como devolver erros com contexto, como verificá-los com errors.Is e errors.As e como tratar cada erro uma única vez.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Erros são valores

Uma função Go que pode falhar devolve um error como último resultado. Quem chama verifica na hora.

Saída:

parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax

Esse é o mecanismo inteiro. Não há exceções, nem try ou catch, nem fluxo de controle escondido: um erro só vai aonde o seu código o leva. O custo é a repetição visível de if err != nil. O benefício é que todo ponto de falha fica visível na página, e você decide em cada um o que acontece.

O tipo error

error é uma interface embutida com um único método:

type error interface {
	Error() string
}

Qualquer tipo com um método Error() string é um erro. Um erro nil significa sucesso. Imprimir um erro com fmt.Println(err) ou %v chama Error().

Criando erros

Duas funções cobrem a maioria dos casos.

errors.New cria um erro com texto fixo. fmt.Errorf formata um, com os mesmos verbos do Printf. Por convenção, strings de erro começam com letra minúscula e não têm pontuação no final, porque normalmente ficam embutidas em mensagens mais longas: load config: open app.yaml: no such file or directory.

O padrão if err != nil

O formato idiomático é: chamar, verificar, retornar cedo. O caminho de sucesso fica na margem esquerda, e cada falha sai assim que acontece.

func loadUser(id int) (*User, error) {
	row, err := db.Query(id)
	if err != nil {
		return nil, err
	}
	u, err := parseUser(row)
	if err != nil {
		return nil, err
	}
	if err := u.Validate(); err != nil {
		return nil, err
	}
	return u, nil
}

Duas convenções para reparar:

  • Em caso de erro, devolva o valor zero nos outros resultados (nil, 0, ""). Quem chama não deve usá-los quando err != nil.
  • if err := f(); err != nil restringe err ao if quando a função só devolve um erro. Isso mantém o escopo externo limpo.

Evite o else depois de um retorno de erro. if err != nil { return err } else { ... } só indenta o caminho feliz sem motivo.

Acrescentando contexto ao devolver um erro

Um erro repassado sem alteração perde a história de onde veio. open config.yaml: no such file or directory não diz qual etapa da inicialização falhou. Acrescente contexto com fmt.Errorf e o verbo %w:

Saída:

start server: read config: open /etc/myapp/config.yaml: no such file or directory
true

Cada camada acrescenta o que estava fazendo, e a mensagem final se lê como uma trilha do topo da chamada até a causa. Um bom contexto nomeia a operação e a entrada: parse line 12, fetch user 42. Não acrescente "error" ou "failed" em cada nível; a mensagem já é um erro.

%w empacota: ele mantém o erro original dentro do novo, então errors.Is e errors.As ainda conseguem encontrá-lo. %v só copia o texto. Use %v quando quiser de propósito esconder um detalhe de implementação de quem chama, por exemplo para que não passem a depender do tipo de erro de um driver de banco de dados.

Verificando erros específicos: errors.Is e errors.As

Às vezes quem chama precisa reagir a um tipo de falha: um arquivo ausente significa "usar os padrões", um timeout significa "tentar de novo". Duas funções respondem a isso, e as duas olham através de todas as camadas de empacotamento.

Regras práticas:

  • Compare com valores de erro predefinidos (sentinelas como io.EOF, os.ErrNotExist, sql.ErrNoRows) usando errors.Is, não ==. O == falha assim que o erro é empacotado.
  • Extraia um erro tipado com errors.As, não com type assertion, pelo mesmo motivo. errors.As recebe um ponteiro para uma variável do tipo alvo.
  • Nunca compare pelo texto de err.Error(). As mensagens mudam entre versões, e a comparação de texto quebra em silêncio quando isso acontece.

Definir os seus próprios erros sentinela e tipos de erro, e juntar vários erros com errors.Join, está em erros personalizados.

Trate cada erro uma única vez

Um erro deve ser tratado exatamente uma vez. Tratar significa uma destas coisas: devolvê-lo (normalmente empacotado), registrá-lo em log e seguir em frente, tentar de novo, ou transformá-lo em uma resposta para o usuário. Fazer duas delas é o bug de erro mais comum em código Go.

// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
	log.Printf("could not fetch user: %v", err)
	return err
}

// Right: add context and return. The top of the program logs once.
if err != nil {
	return fmt.Errorf("fetch user %d: %w", id, err)
}

Registrar em log e devolver produz a mesma falha várias vezes nos logs, cada uma com menos contexto que a mensagem final. Deixe os erros subirem até o lugar que pode decidir o que fazer (um handler HTTP, um main, um laço de worker) e registre lá.

Onde os erros terminam

No topo do programa, algo precisa agir sobre o erro. No main, isso normalmente significa imprimi-lo e sair com um status diferente de zero:

Executar isto sem argumentos imprime error: usage: app <name> no stderr e sai com status 1 (digite um nome no painel Args para ver o outro caminho). Manter o main nesse formato, com o trabalho real em run, deixa o programa testável e garante que as instruções defer dentro de run ainda executem, já que o os.Exit pula as chamadas adiadas.

Em um servidor HTTP, o topo é o handler: ele converte o erro em um código de status e em uma mensagem segura para o cliente, e registra a mensagem detalhada para você.

Erros que você pode ignorar, e os que não pode

Às vezes ignorar um erro é o certo, mas deixe isso explícito com _, para que quem lê saiba que foi uma decisão:

_ = conn.SetDeadline(t) // best effort

Algumas chamadas não falham na prática (strings.Builder.WriteString, bytes.Buffer.Write). Outras parecem inofensivas e não são: o Close de um arquivo em que você escreveu pode reportar que os dados nunca chegaram ao disco, e json.Marshal falha com channels e funções. Na dúvida, verifique.

O linter errcheck (incluído no golangci-lint) reporta erros não verificados. O go vet sozinho não os aponta.

Erros e panics

Go também tem panic, mas ele não é um sistema de exceções. Use erros para tudo o que pode dar errado na operação normal: entrada inválida, arquivos ausentes, falhas de rede. Reserve o panic para bugs (um estado impossível, uma invariante quebrada) e para falhas na inicialização em que continuar não faz sentido. Uma biblioteca quase nunca deve causar panic através da sua API. Veja panic e recover.

Reduzindo a repetição

if err != nil é verboso, e propostas de nova sintaxe para ele foram recusadas repetidas vezes; o time do Go anunciou em 2025 que não vai mais buscar mudanças de sintaxe para o tratamento de erros. Alguns padrões reduzem o ruído dentro da própria linguagem:

  • Retorne cedo e mantenha as funções pequenas. A maior parte da repetição vem de funções longas que fazem muitas etapas.
  • O erro persistente (sticky error). Para uma sequência de escritas, guarde o primeiro erro em um campo da struct e faça as chamadas seguintes não fazerem nada depois que ele for definido. O bufio.Writer funciona assim: você verifica o erro uma vez depois do Flush.
  • Empacote uma vez por função. Uma closure adiada sobre um resultado nomeado pode acrescentar o mesmo contexto a todo erro que a função devolve (veja defer).

Erros comuns

  • Usar um valor quando err não é nil. Verifique primeiro, depois use.
  • Registrar em log e devolver. Escolha um.
  • Comparar com == depois de empacotar. Use errors.Is.
  • Perder a causa com %v. Use %w, a menos que esconder seja o objetivo.
  • Devolver um ponteiro nil tipado como error. var e *MyErr; return e não é nil para quem chama. Devolva um nil literal.
  • Mensagens com maiúscula ou pontuação. errors.New("Failed to connect.") fica ruim depois de empacotado. Escreva connect to db: ....

Perguntas frequentes

Como funciona o tratamento de erros em Go?

Funções que podem falhar devolvem um error como último resultado. Quem chama verifica na hora: v, err := f(); if err != nil { return err }. Um error é um valor de interface comum com um só método, Error() string, e nil significa sucesso. Não existem exceções.

Go tem try/catch?

Não. Go não tem exceções nem try/catch. Falhas esperadas são devolvidas como valores error e verificadas com if err != nil. panic e recover existem, mas são para bugs de programação e estados irrecuperáveis, não para o fluxo normal de erros.

Como devolver um erro em Go?

Declare error como último resultado e devolva nil em caso de sucesso. Crie erros com errors.New("mensagem") para texto fixo ou com fmt.Errorf("reading %s: %w", name, err) para acrescentar contexto a um erro recebido. Em caso de falha, devolva valores zero nos outros resultados.

Qual a diferença entre %w e %v no fmt.Errorf?

Os dois colocam a mensagem do erro original no novo. %w também o empacota, então errors.Is e errors.As ainda conseguem encontrar o original. %v gera um erro novo só com o texto. Use %w quando quem chama pode precisar verificar a causa, e %v quando quiser escondê-la.

Como verificar qual erro foi devolvido em Go?

Use errors.Is(err, target) para comparar com um erro sentinela como io.EOF ou os.ErrNotExist, e errors.As(err, &target) para extrair um tipo de erro específico como *fs.PathError. Os dois percorrem os erros empacotados. Evite comparar strings de err.Error().

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR