Erros em Go são valores comuns que você retorna e confere. Aprenda a envolvê-los com contexto, achar a causa com errors.Is e errors.As, juntar vários de uma vez e deixar panic e recover para bugs de verdade.
Go não tem exceções para falhas comuns. Uma função que pode falhar retorna um erro como último resultado, e quem chama decide o que fazer com ele. Isso deixa o tratamento de erros visível em cada linha, e faz as ferramentas para adicionar contexto e encontrar a causa importarem muito.
Este post cobre essas ferramentas, de errors.New passando por wrapping, errors.Is, errors.As e errors.Join, e depois panic e recover, que servem para outra coisa. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.
Um erro é qualquer valor com um método Error
O tipo error em Go é uma interface embutida com um único método, Error() string. Qualquer tipo que tenha esse método é um erro. A biblioteca padrão dá dois jeitos rápidos de criar um: errors.New para uma mensagem fixa, e fmt.Errorf quando a mensagem precisa ter valores dentro.
package main
import (
"errors"
"fmt"
"strconv"
)
func parseAge(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("age %q is not a number", s)
}
if n < 0 {
return 0, errors.New("age can't be negative")
}
return n, nil
}
func main() {
for _, in := range []string{"42", "-3", "old"} {
age, err := parseAge(in)
if err != nil {
fmt.Println("error:", err)
continue
}
fmt.Println("age:", age)
}
var err error = errors.New("disk full")
fmt.Printf("%T\n", err)
}
Ele imprime:
age: 42
error: age can't be negative
error: age "old" is not a number
*errors.errorString
parseAge retorna um valor e um erro. Quando dá certo, o erro é nil. Quando falha, o valor é 0 e o erro diz o que deu errado. Quem chama confere if err != nil logo depois da chamada e resolve isso antes de mexer em age.
A última linha mostra o que errors.New cria de verdade: um ponteiro para uma pequena struct não exportada que guarda a mensagem. Você nunca precisa desse tipo pelo nome. Só precisa do método Error, e fmt.Println o chama por você.
Por que Go obriga você a conferir
O formato if err != nil { return err } aparece em todo lugar em Go, e é de propósito. Uma exceção pode pular para fora de qualquer chamada, então lendo uma função você não sabe quais linhas podem fazê-la sair antes da hora. Em Go, toda linha que pode falhar tem a conferência logo abaixo. O caminho de falha é código comum, que você consegue ler, acompanhar passo a passo e testar.
O custo é digitar mais. O benefício é que nada sai de uma função sem você ver por onde. E, como a parte sobre funções mostrou, você não consegue descartar um erro sem querer: para ignorar um, você precisa escrever _, onde quem revisa consegue ver.
Erros sentinela e errors.Is
Um erro sentinela é um valor de erro no nível do pacote com o qual quem chama compara. Por convenção, o nome começa com Err. Você o cria uma vez com errors.New e retorna esse mesmo valor toda vez que a condição acontece.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
var users = map[int]string{1: "ada", 2: "linus"}
func findUser(id int) (string, error) {
name, ok := users[id]
if !ok {
return "", ErrNotFound
}
return name, nil
}
func main() {
_, err := findUser(7)
fmt.Println(err)
fmt.Println(err == ErrNotFound)
fmt.Println(errors.Is(err, ErrNotFound))
same := errors.New("not found")
fmt.Println(errors.Is(err, same))
}
Ele imprime:
not found
true
true
false
err == ErrNotFound é verdadeiro, porque findUser retornou exatamente esse valor. errors.Is concorda. A última linha é a que merece atenção: um segundo erro com o mesmo texto não é o mesmo erro. errors.New cria um ponteiro novo a cada vez, e erros são comparados por identidade, não por mensagem. Então nunca confira um erro comparando a string dele.
A biblioteca padrão usa sentinelas por toda parte. io.EOF quer dizer “não há mais entrada”, e sql.ErrNoRows quer dizer que uma consulta não encontrou nada. Por enquanto == e errors.Is dão a mesma resposta. Eles deixam de concordar assim que alguém adiciona contexto, e esse é o assunto da próxima seção.
Wrapping: adicionando contexto com %w
Um “not found” solto saindo de um programa grande diz quase nada. Não encontrado onde? Fazendo o quê? A resposta de Go é envolver o erro: cada função que o repassa para cima adiciona uma nota curta sobre o que estava fazendo. fmt.Errorf faz o wrapping quando você usa o verbo %w.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func openFile(name string) error {
return ErrNotFound
}
func readConfig(name string) error {
if err := openFile(name); err != nil {
return fmt.Errorf("open %s: %w", name, err)
}
return nil
}
func startServer() error {
if err := readConfig("config.json"); err != nil {
return fmt.Errorf("read config: %w", err)
}
return nil
}
func main() {
err := startServer()
if err != nil {
err = fmt.Errorf("start server: %w", err)
}
fmt.Println(err)
fmt.Println(err == ErrNotFound)
fmt.Println(errors.Is(err, ErrNotFound))
for e := err; e != nil; e = errors.Unwrap(e) {
fmt.Printf(" %q\n", e.Error())
}
}
Ele imprime:
start server: read config: open config.json: not found
false
true
"start server: read config: open config.json: not found"
"read config: open config.json: not found"
"open config.json: not found"
"not found"
O erro subiu três níveis, e cada nível colocou suas próprias palavras na frente. A mensagem final se lê da esquerda para a direita, da tarefa mais externa até a causa raiz. Esse é o estilo Go: frases curtas em minúsculas, ligadas por dois-pontos, sem “error:” ou “failed to” em cada nível.
Agora err == ErrNotFound é falso, porque err é um wrapper, não o sentinela. errors.Is continua verdadeiro, porque olha lá dentro. O laço no final mostra por onde ele olha: errors.Unwrap tira uma camada de cada vez até chegar ao original.
Explicado como se você tivesse dez anos
Imagine um bilhete passado de mão em mão numa fila de pessoas. A primeira criança escreve “not found” e entrega para a próxima pessoa.
Essa pessoa não joga o bilhete fora nem o reescreve. Ela coloca o bilhete dentro de um envelope e escreve do lado de fora: “ao abrir config.json:”. A pessoa seguinte coloca esse envelope dentro de um maior e escreve “ao ler a configuração:”. Quando chega à professora, o lado de fora conta a história toda, e o bilhete original continua lá no meio.
errors.Is é a professora abrindo envelope atrás de envelope, procurando o único bilhete que diz “not found”. Não importa quantos envelopes existam.
A versão precisa
fmt.Errorf com %w retorna um erro que guarda tanto a mensagem nova quanto o erro que você passou. Esse wrapper tem um método Unwrap() error que retorna o erro de dentro. A cadeia é uma lista ligada: cada wrapper aponta para o que está dentro dele, e o erro mais interno não tem Unwrap.
errors.Is(err, target) percorre essa lista. A cada passo, ele confere err == target e também chama um método Is(error) bool, se o erro tiver um. Ele retorna verdadeiro no primeiro que casar e falso quando a cadeia acaba. errors.Unwrap faz um passo desse percurso na mão.
Onde a analogia falha: envelopes de verdade escondem o bilhete, mas um wrapper em Go não. A mensagem dele já contém a mensagem de dentro, e é por isso que imprimir o erro de fora mostra a cadeia inteira. E o wrapping não é automático. Se uma função usa %v em vez de %w, ela copia o texto mas joga o envelope fora.
%w ou %v
Os dois verbos imprimem a mesma mensagem, e só um deles mantém a cadeia:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func main() {
wrapped := fmt.Errorf("load user 7: %w", ErrNotFound)
flattened := fmt.Errorf("load user 7: %v", ErrNotFound)
fmt.Println(wrapped)
fmt.Println(flattened)
fmt.Println(errors.Is(wrapped, ErrNotFound))
fmt.Println(errors.Is(flattened, ErrNotFound))
}
Ele imprime:
load user 7: not found
load user 7: not found
true
false
Lendo a saída, você não consegue distinguir os dois, e é isso que torna a diferença fácil de passar despercebida. Use %w quando quem chama pode precisar conferir a causa. Use %v quando você quer escondê-la de propósito, por exemplo para que um detalhe da sua camada de armazenamento não vire parte da API do seu pacote.
Trate um erro uma vez só
Cada erro deve ser tratado uma vez: ou você o retorna, com contexto adicionado, ou você o registra no log e para por ali. Fazer as duas coisas é um hábito comum, e enche os logs com a mesma falha contada várias vezes.
package main
import (
"errors"
"log"
"os"
)
func readConfig() error {
return errors.New("config.json: file not found")
}
func startServer() error {
err := readConfig()
if err != nil {
log.Println("could not read config:", err)
return err
}
return nil
}
func main() {
log.SetFlags(0)
log.SetOutput(os.Stdout)
if err := startServer(); err != nil {
log.Println("server failed:", err)
}
}
Ele imprime:
could not read config: config.json: file not found
server failed: config.json: file not found
(log.SetFlags(0) desliga o horário para a saída ser a mesma em toda execução.)
Uma falha, duas linhas de log. Num programa real com cinco camadas, são cinco linhas, muitas vezes distantes umas das outras no log, e quem lê às 3 da manhã conta cinco problemas. Pior: a segunda linha perdeu o contexto que a primeira tinha.
A correção é escolher uma das duas. startServer deve fazer return fmt.Errorf("read config: %w", err) e não registrar nada. main é o topo, então não tem para onde retornar. Ela registra uma vez, e essa única linha diz server failed: read config: config.json: file not found, com a história toda em ordem.
Tipos de erro próprios e errors.As
Um tipo de erro próprio carrega dados estruturados com os quais quem chama pode agir, não só uma mensagem. Quando quem chama precisa saber qual campo falhou na validação, ou qual status code uma chamada HTTP retornou, um sentinela não basta. Você define uma struct com os campos e dá a ela um método Error.
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Reason string
}
func (e *ValidationError) Error() string {
return e.Field + ": " + e.Reason
}
func validate(email string) error {
if email == "" {
return &ValidationError{Field: "email", Reason: "is required"}
}
return nil
}
func createUser(email string) error {
if err := validate(email); err != nil {
return fmt.Errorf("create user: %w", err)
}
return nil
}
func main() {
err := createUser("")
fmt.Println(err)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("field:", ve.Field)
fmt.Println("reason:", ve.Reason)
}
if ve, ok := errors.AsType[*ValidationError](err); ok {
fmt.Println("AsType found field:", ve.Field)
}
}
Ele imprime:
create user: email: is required
field: email
reason: is required
AsType found field: email
errors.Is pergunta “este valor específico está em algum lugar da cadeia?”. errors.As pergunta “existe um erro deste tipo em algum lugar da cadeia? Se existe, me dá ele”. Você declara uma variável do tipo que quer, passa o endereço dela, e errors.As percorre a cadeia. Quando encontra um *ValidationError, guarda-o em ve e retorna verdadeiro. O wrap feito por createUser não atrapalhou.
O ponteiro para ve confunde as pessoas. errors.As precisa de um lugar para colocar o que encontra, então recebe &ve, que é um **ValidationError. Passe o próprio ve e o go vet barra você.
O Go 1.26 adiciona errors.AsType, uma versão genérica que retorna o que encontrou e um bool, sem variável para declarar antes. Ela faz o mesmo percurso. Você vai ver errors.As em quase todo código existente, e AsType em código novo, conforme ele pegar.
Um alerta quando você escrever funções como validate. Retorne um nil puro quando der certo, nunca um *ValidationError nil guardado num error. Esse segundo não é igual a nil, e essa é a armadilha da interface nil da parte sobre interfaces.
errors.Join: vários erros de uma vez
Às vezes uma chamada tem várias coisas erradas e independentes, e informar só a primeira faz o usuário corrigir uma de cada vez. errors.Join, que chegou no Go 1.20, combina erros em um só.
package main
import (
"errors"
"fmt"
)
var ErrTooShort = errors.New("password too short")
func checkSignup(name, password string) error {
var errs []error
if name == "" {
errs = append(errs, errors.New("name is required"))
}
if len(password) < 8 {
errs = append(errs, ErrTooShort)
}
return errors.Join(errs...)
}
func main() {
err := checkSignup("", "abc")
fmt.Println(err)
fmt.Println("---")
fmt.Println(errors.Is(err, ErrTooShort))
fmt.Println(checkSignup("ada", "correct horse"))
}
Ele imprime:
name is required
password too short
---
true
<nil>
A mensagem do erro combinado coloca cada erro na própria linha. errors.Is e errors.As procuram em todos os ramos, então a conferência de ErrTooShort continua funcionando.
A última linha é a parte útil. errors.Join ignora erros nil, e quando não sobra nenhum ele retorna nil. Então você pode juntar problemas num slice e retornar errors.Join(errs...) sem conferir se o slice está vazio.
panic é para bugs, não para erros
Um panic interrompe o fluxo normal de uma goroutine. As chamadas adiadas ainda rodam, e depois, a não ser que algo se recupere, o programa inteiro quebra com uma mensagem e um stack trace. Esse é o resultado certo para um bug: algo que o programador errou, ou um estado que o código acredita ser impossível. Um arquivo que falta, uma entrada ruim do usuário ou um timeout de rede não é bug. Isso são erros, e erros são retornados.
package main
import "fmt"
type Direction int
const (
North Direction = iota
South
)
func (d Direction) String() string {
switch d {
case North:
return "north"
case South:
return "south"
}
panic(fmt.Sprintf("unknown Direction %d", int(d)))
}
func main() {
fmt.Println(North.String())
fmt.Println(Direction(9).String())
}
Imprime duas linhas e para:
north
panic: unknown Direction 9
Depois dessas duas linhas vêm goroutine 1 [running]: e um stack trace apontando para a linha do panic, e o programa sai com status 2. Só existem duas direções, então Direction(9) quer dizer que algum código, em algum lugar, construiu um valor que não devia. Retornar um erro aqui obrigaria todo mundo que chama String a tratar um caso que só um bug causa. Um panic diz “corrija o código” em voz alta, bem onde deu errado.
O runtime entra em panic pelo mesmo tipo de motivo: indexar além do fim de um slice, escrever num map nil, dividir um inteiro por zero. Você já conheceu o primeiro deles na parte sobre slices.
recover: transformando um panic de volta em erro
recover é uma função embutida que interrompe um panic em andamento e retorna o valor que foi passado para panic. Ela só tem efeito quando é chamada diretamente de uma função adiada enquanto a goroutine está em panic. O uso normal é numa fronteira: código que roda a função de outra pessoa e não quer que o bug dela derrube tudo.
package main
import (
"errors"
"fmt"
)
func safeRun(name string, job func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("job %s panicked: %v", name, r)
}
}()
job()
return nil
}
func main() {
err := safeRun("ok", func() {
fmt.Println("running ok")
})
fmt.Println("result:", err)
err = safeRun("bad", func() {
var counts map[string]int
counts["x"]++
})
fmt.Println("result:", err)
err = safeRun("custom", func() {
panic(errors.New("state machine reached an impossible state"))
})
fmt.Println("result:", err)
fmt.Println("main carries on")
}
Ele imprime:
running ok
result: <nil>
result: job bad panicked: assignment to entry in nil map
result: job custom panicked: state machine reached an impossible state
main carries on
O primeiro job rodou normalmente. recover retornou nil, então a função adiada não mudou nada. O segundo job escreveu num map nil, o que faz o runtime entrar em panic. O panic subiu para fora de job(), a função adiada rodou, recover o capturou, e a closure colocou um erro no resultado nomeado err. É o mesmo truque da closure adiada da parte sobre funções. O terceiro job entrou em panic de propósito e foi capturado do mesmo jeito.
É exatamente isso que o servidor net/http de Go faz para cada requisição. Um handler que entra em panic tem a requisição descartada e registrada no log, e o servidor continua atendendo todo o resto. Você vai ver isso na parte sobre servidores HTTP.
Não use recover para construir exceções a partir de panic. Se uma função pode falhar de um jeito normal, retorne um erro. Use recover só numa fronteira onde uma quebra seria pior do que uma falha registrada no log.
Onde recover não funciona
Uma chamada a recover só interrompe um panic quando uma função adiada a chama diretamente. Chamada em qualquer outro lugar, ela não faz nada e retorna nil.
package main
import "fmt"
func helper() {
if r := recover(); r != nil {
fmt.Println("helper recovered:", r)
}
}
func main() {
fmt.Println("recover outside a panic:", recover())
defer func() {
helper()
fmt.Println("the deferred function returns")
}()
panic("boom")
}
Imprime três linhas e para:
recover outside a panic: <nil>
the deferred function returns
panic: boom
O primeiro recover rodou quando nada estava em panic, então retornou nil. O segundo fica dentro de helper, e helper é chamada pela função adiada, em vez de ser a função adiada. Esse único nível de indireção basta: recover retorna nil, helper não imprime nada, e o panic segue em frente e derruba o programa. Mover a chamada de recover para dentro da própria closure adiada o capturaria.
O outro limite são as goroutines. Um recover adiado só captura panics da própria goroutine:
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("main recovered:", r)
}
}()
go func() {
panic("worker failed")
}()
select {} // wait forever; the worker's panic ends the program first
}
Imprime uma linha e para:
panic: worker failed
main tem um recover perfeitamente bom, e ele nunca roda. O panic aconteceu em outra goroutine, que não tem um recover adiado próprio, e um panic não recuperado em qualquer goroutine derruba o programa inteiro. Por isso, código que inicia goroutines para rodar trabalho não confiável coloca o defer–recover dentro de cada goroutine. A parte sobre goroutines mostra como iniciá-las e esperar por elas do jeito certo.
Relançando o panic
Uma função adiada às vezes recupera um panic, olha para ele e decide que, afinal, não consegue tratá-lo. Ela pode registrar o que sabe e chamar panic de novo com o mesmo valor.
package main
import "fmt"
func main() {
defer func() {
r := recover()
fmt.Println("logging, then panicking again:", r)
panic(r)
}()
panic("invariant broken: balance below zero")
}
Imprime duas linhas e para:
logging, then panicking again: invariant broken: balance below zero
panic: invariant broken: balance below zero [recovered, repanicked]
Olhe o fim da segunda linha. Quando um panic é recuperado e o mesmo valor é relançado, a mensagem de quebra diz [recovered, repanicked] e imprime o valor uma vez só. Se você vir essa marca numa quebra, algum código mais acima na pilha capturou o panic e deixou ele seguir.
Qual devo usar?
A escolha se resume a quem precisa reagir, e como.
| Situação | Use | Quem chama confere com |
|---|---|---|
| Algo falhou, e quem chama só precisa saber que falhou | Retorne um erro de errors.New ou fmt.Errorf, com wrapping via %w |
err != nil |
| Quem chama precisa reconhecer uma condição específica, como “não encontrado” | Um sentinela: var ErrNotFound = errors.New(...) |
errors.Is |
| Quem chama precisa de detalhes: qual campo, qual código, qual espera até tentar de novo | Um tipo de erro próprio com campos | errors.As ou errors.AsType |
| Várias coisas independentes falharam de uma vez | errors.Join |
errors.Is ou errors.As, que procuram em todas as partes |
| Um bug ou um estado impossível, algo que só uma mudança no código corrige | panic |
Nada. Corrija o código. Use recover só numa fronteira |
O que lembrar
erroré uma interface com um método,Error() string. Retorne-o como último resultado e confiraif err != nillogo depois da chamada.- Trate cada erro uma vez: retorne com contexto, ou registre no log. Não os dois.
- Faça o wrapping com
fmt.Errorf("doing X: %w", err). A mensagem vai crescendo nível a nível, e%vperderia a cadeia. errors.Isencontra um valor específico em qualquer ponto da cadeia. Compare erros com ele, não com==, e nunca pelo texto da mensagem.errors.As, ouerrors.AsTypea partir do Go 1.26, tira um tipo de erro próprio da cadeia para você ler os campos dele.errors.Joincombina vários erros e retornanilquando todos sãonil.panicé para bugs.recoversó funciona quando chamado diretamente numa função adiada da goroutine em panic, e o lugar dele é nas fronteiras.
Toda vez que um erro sobe um nível, adicione o que você estava fazendo e mantenha o original lá dentro.