Tratamento de Erros em Zig: Guia Completo
O sistema de erros de Zig é uma das características mais distintivas desta linguagem de programação de sistemas. Diferente de exceptions (Java, C++, Python) ou Result types (Rust), Zig adota uma abordagem única baseada em error sets e error unions que é explícita, eficiente e integrada à linguagem.
Neste guia completo, você vai aprender tudo sobre tratamento de erros em Zig: desde os conceitos básicos até padrões avançados usados em código de produção.
Pré-requisitos: Conhecimento básico de Zig (variáveis, funções, tipos). Recomendamos ter lido Structs, Enums e Unions antes deste guia.
Índice
- Visão Geral do Sistema de Erros
- Error Sets
- Error Unions
- Propagando Erros com
try - Tratando Erros com
catch - Valores Padrão com
orelse errdefer— Defer Condicional- Erros em Métodos e Structs
- Error Inference
- Padrões Comuns
- Comparação com Outras Linguagens
- Melhores Práticas
- FAQ
- Próximos Passos
Visão Geral do Sistema de Erros
A Filosofia Zig
Zig segue o princípio de “erros são valores”, não exceções. Isso significa:
- Erros são explícitos — toda função que pode falhar declara isso no tipo de retorno
- Não há exceções — não existe mecanismo de unwinding ou catch-all
- Zero overhead — tratamento de erros não custa em runtime quando não falha
- Composição simples — é fácil combinar e propagar erros
Comparativo Rápido
| Aspecto | Exceptions (Java/C++) | Result<T,E> (Rust) | Zig Error Unions |
|---|---|---|---|
| Erro é… | Exception lançada | Enum Ok/Err | Valor especial |
| Declaração | throws (opcional) | Result<T,E> | !T ou E!T |
| Propagação | Automaticamente | ? operator | try keyword |
| Overhead | Alto (stack unwinding) | Baixo | Zero |
| Type safety | Runtime | Compile time | Compile time |
Error Sets
Error sets são conjuntos nomeados de erros possíveis. Funcionam como enums, mas são dedicados a erros.
Declaração Básica
const std = @import("std");
// Definindo um error set
const ErroArquivo = error{
NaoEncontrado,
PermissaoNegada,
EmUso,
EspacoInsuficiente,
};
// Error set vazio (nenhum erro possível)
const SemErro = error{};
Usando Error Sets
// Função que retorna um erro específico
fn abrirArquivo(caminho: []const u8) ErroArquivo!Arquivo {
if (caminho.len == 0) return error.NaoEncontrado;
// ... código real ...
return Arquivo{};
}
// Struct de arquivo (exemplo)
const Arquivo = struct {
handle: i32,
};
Error Set Universal: anyerror
anyerror é o error set que contém todos os erros possíveis:
// Pode retornar qualquer erro
fn operacaoArriscada() anyerror!void {
return error.AlgumErro;
}
// Útil para callbacks genéricos
const CallbackErro = fn () anyerror!void;
Error Unions
Error unions representam “um valor OU um erro”. São o coração do sistema de erros em Zig.
Sintaxe
// T!E significa: valor do tipo T ou erro do error set E
const resultado: i32!ErroArquivo = 42;
const erro: i32!ErroArquivo = error.NaoEncontrado;
// Erro set implícito (infere do contexto)
const valor: !i32 = calcular(); // !i32 = anyerror!i32
Criando Error Unions
// Sucesso: retorna o valor diretamente
fn sucesso() !i32 {
return 42;
}
// Erro: retorna um valor do error set
fn falha() !i32 {
return error.Falha;
}
// Múltiplos erros possíveis
const MeuErro = error{ ErroA, ErroB };
const OutroErro = error{ ErroC };
fn podeFalharDeVariasFormas() (MeuErro || OutroErro)!i32 {
// Pode retornar ErroA, ErroB ou ErroC
return error.ErroA;
}
Acessando Valores de Error Unions
const resultado = podeFalhar();
// 1. If com capture
if (resultado) |valor| {
std.debug.print("Sucesso: {d}\n", .{valor});
} else |err| {
std.debug.print("Erro: {}\n", .{err});
}
// 2. Switch
switch (resultado) {
.Ok => |valor| std.debug.print("Valor: {d}\n", .{valor}),
.Err => |err| std.debug.print("Erro: {}\n", .{err}),
}
// 3. While com erro (para iteradores que falham)
while (iterador.next()) |item| {
// processa item
} else |err| {
// iterador falhou
}
Propagando Erros com try
A palavra-chave try é a forma mais comum de propagar erros. Se o valor for um erro, retorna imediatamente da função.
Sintaxe Básica
const std = @import("std");
const ErroOperacao = error{ DivisaoPorZero, Overflow };
fn divisaoSegura(a: i32, b: i32) ErroOperacao!i32 {
if (b == 0) return error.DivisaoPorZero;
return @divTrunc(a, b);
}
// Usando try
fn calcularMedia(a: i32, b: i32) ErroOperacao!i32 {
const soma = try adicionar(a, b); // Propaga erro se falhar
return try divisaoSegura(soma, 2); // Propaga erro se falhar
}
fn adicionar(a: i32, b: i32) ErroOperacao!i32 {
const resultado = @addWithOverflow(a, b);
if (resultado[1] != 0) return error.Overflow;
return resultado[0];
}
Como try Funciona
// Isso:
const x = try funcao();
// É equivalente a:
const x = funcao() catch |err| return err;
Cadeias de try
fn processarDados() !void {
const arquivo = try abrirArquivo("dados.txt");
const conteudo = try lerArquivo(arquivo);
const parseado = try parseJSON(conteudo);
try salvarNoBanco(parseado);
}
Se qualquer operação falhar, a função retorna imediatamente com o erro.
Tratando Erros com catch
catch permite tratar erros localmente, sem propagar.
Sintaxe Básica
// catch com valor padrão
const resultado = podeFalhar() catch 0;
// catch com bloco
const resultado = podeFalhar() catch |err| {
std.debug.print("Erro: {}\n", .{err});
0 // valor padrão
};
Exemplos Práticos
const std = @import("std");
const ErroConfig = error{ ConfigNaoEncontrada, ConfigInvalida };
fn carregarConfig(caminho: []const u8) ErroConfig!Config {
// ... tenta carregar ...
return Config{};
}
const Config = struct {
porta: u16 = 8080,
host: []const u8 = "localhost",
};
pub fn main() void {
// Valor padrão simples
const config = carregarConfig("app.conf") catch Config{
.porta = 3000,
.host = "0.0.0.0",
};
// Tratamento específico
const porta = parsePorta("8080") catch |err| {
std.debug.print("Porta inválida: {}\n", .{err});
8080 // fallback
};
// Log e re-trow
const dados = buscarDados() catch |err| {
std.log.err("Falha ao buscar dados: {}", .{err});
return; // ou: return err;
};
}
fn parsePorta(texto: []const u8) !u16 {
return std.fmt.parseInt(u16, texto, 10);
}
fn buscarDados() ![]const u8 {
return "dados";
}
Catch com Unreachable
Quando você sabe que uma operação não vai falhar (mas o compilador não sabe):
// "Eu garanto que isso não falha"
const numero = parseInt("42", 10) catch unreachable;
// ⚠️ Use com cuidado! Falha = comportamento indefinido
Valores Padrão com orelse
orelse é similar a catch, mas específico para optional types (?T).
Sintaxe
const talvezValor: ?i32 = null;
// orelse com valor
const valor = talvezValor orelse 0;
// orelse com bloco
const valor = talvezValor orelse {
std.debug.print("Usando padrão\n");
0
};
// orelse com retorno
const valor = talvezValor orelse return error.ValorNulo;
Combinando com try e catch
const std = @import("std");
const Usuario = struct {
id: u64,
nome: []const u8,
email: ?[]const u8, // opcional
};
fn enviarEmail(usuario: Usuario) !void {
// orelse para optional
const email = usuario.email orelse {
std.debug.print("Usuário sem email\n");
return;
};
// try para error union
try enviarPara(email);
}
fn enviarPara(email: []const u8) !void {
_ = email;
// ...
}
errdefer — Defer Condicional
errdefer executa código somente se a função retornar um erro. É essencial para cleanup em funções que falham.
Problema: Sem errdefer
fn processarArquivos() !void {
const arquivo1 = try abrir("a.txt");
// Se falhar aqui, arquivo1 não é fechado!
const arquivo2 = try abrir("b.txt");
// Se falhar aqui, arquivo2 não é fechado!
// Processamento...
arquivo2.fechar();
arquivo1.fechar();
}
Solução: Com errdefer
fn processarArquivos() !void {
const arquivo1 = try abrir("a.txt");
errdefer arquivo1.fechar(); // Só executa se der erro
const arquivo2 = try abrir("b.txt");
errdefer arquivo2.fechar(); // Só executa se der erro
// Processamento (pode falhar)...
try processar(arquivo1, arquivo2);
// Se chegou aqui, deu tudo certo
// errdefers NÃO executam
arquivo2.fechar();
arquivo1.fechar();
}
const Arquivo = struct {
nome: []const u8,
fn abrir(nome: []const u8) !Arquivo {
return Arquivo{ .nome = nome };
}
fn fechar(self: Arquivo) void {
std.debug.print("Fechando: {s}\n", .{self.nome});
}
};
const std = @import("std");
fn processar(a: Arquivo, b: Arquivo) !void {
_ = a; _ = b;
// ...
}
errdefer com Capture
fn operacaoComplexa() !void {
const recurso = try alocar();
errdefer |err| {
// Acesso ao erro que causou o defer
std.log.err("Cleanup devido a: {}", .{err});
liberar(recurso);
}
try usar(recurso);
}
fn alocar() !i32 { return 42; }
fn liberar(x: i32) void { _ = x; }
fn usar(x: i32) !void { _ = x; }
Ordem de Execução
fn demonstrarOrdem() !void {
errdefer std.debug.print("errdefer 1\n");
errdefer std.debug.print("errdefer 2\n");
defer std.debug.print("defer 1\n");
defer std.debug.print("defer 2\n");
return error.Falha;
// Imprime:
// errdefer 2
// errdefer 1
}
Regra: errdefers executam na ordem inversa (LIFO), antes de retornar o erro.
Erros em Métodos e Structs
Métodos que Falham
const std = @import("std");
const Contador = struct {
valor: i32,
maximo: i32,
const Erro = error{ LimiteExcedido };
pub fn incrementar(self: *Contador) Erro!void {
if (self.valor >= self.maximo) {
return error.LimiteExcedido;
}
self.valor += 1;
}
pub fn novo(maximo: i32) Contador {
return .{
.valor = 0,
.maximo = maximo,
};
}
};
pub fn main() !void {
var contador = Contador.novo(5);
var i: i32 = 0;
while (i < 10) : (i += 1) {
contador.incrementar() catch |err| {
std.debug.print("Erro na iteração {d}: {}\n", .{ i, err });
break;
};
}
std.debug.print("Valor final: {d}\n", .{contador.valor});
}
Error Set como Campo
const ResultadoOperacao = struct {
sucesso: bool,
erro: ?ErroDetalhado,
dados: ?[]const u8,
};
const ErroDetalhado = struct {
codigo: u32,
mensagem: []const u8,
timestamp: i64,
};
Error Inference
Zig pode inferir error sets automaticamente em muitos casos.
Inferência Automática
// O compilador infere: error{DivisaoPorZero}!i32
fn dividir(a: i32, b: i32) !i32 {
if (b == 0) return error.DivisaoPorZero;
return @divTrunc(a, b);
}
// Error set combinado automaticamente
fn operacaoComplexa() !void {
const x = try dividir(10, 2); // pode dar DivisaoPorZero
const y = try raizQuadrada(x); // pode dar RaizNegativa
// Error set resultante: {DivisaoPorZero, RaizNegativa}
}
fn raizQuadrada(x: i32) !i32 {
if (x < 0) return error.RaizNegativa;
return 0;
}
Inferência em Loops
// Error set é inferido de todas as iterações possíveis
fn processarLista(itens: []const i32) !void {
for (itens) |item| {
try processar(item); // erro pode vir de qualquer iteração
}
}
Inferência Recursiva
fn fibonacci(n: u32) error{Overflow}!u32 {
if (n <= 1) return n;
const a = try fibonacci(n - 1);
const b = try fibonacci(n - 2);
return std.math.add(u32, a, b) catch return error.Overflow;
}
const std = @import("std");
Padrões Comuns
1. Wrap Errors (Encapsulamento)
const ErroBaixoNivel = error{ Io, Timeout };
const ErroAltoNivel = error{ CarregamentoFalhou };
fn carregarDados() ErroAltoNivel!Dados {
const arquivo = abrirArquivo() catch |err| {
std.log.err("Falha de IO: {}", .{err});
return error.CarregamentoFalhou;
};
_ = arquivo;
return Dados{};
}
const Dados = struct {};
fn abrirArquivo() ErroBaixoNivel!i32 { return 0; }
const std = @import("std");
2. Early Return
fn processarEntrada(entrada: []const u8) !Resultado {
if (entrada.len == 0) return error.EntradaVazia;
if (entrada.len > 1000) return error.EntradaMuitoGrande;
const parseado = try parse(entrada);
const validado = try validar(parseado);
return try transformar(validado);
}
const Resultado = struct {};
fn parse(e: []const u8) !i32 { _ = e; return 0; }
fn validar(x: i32) !i32 { _ = x; return 0; }
fn transformar(x: i32) !Resultado { _ = x; return Resultado{}; }
3. Accumulate Errors
const std = @import("std");
const ErroValidacao = error{
NomeVazio,
EmailInvalido,
SenhaCurta,
};
const ErrosValidacao = struct {
erros: [3]?ErroValidacao,
count: usize,
pub fn novo() ErrosValidacao {
return .{
.erros = .{ null, null, null },
.count = 0,
};
}
pub fn adicionar(self: *ErrosValidacao, erro: ErroValidacao) void {
if (self.count < self.erros.len) {
self.erros[self.count] = erro;
self.count += 1;
}
}
pub fn temErros(self: ErrosValidacao) bool {
return self.count > 0;
}
};
fn validarUsuario(nome: []const u8, email: []const u8, senha: []const u8) ErrosValidacao {
var erros = ErrosValidacao.novo();
if (nome.len == 0) erros.adicionar(error.NomeVazio);
if (!contemArroba(email)) erros.adicionar(error.EmailInvalido);
if (senha.len < 8) erros.adicionar(error.SenhaCurta);
return erros;
}
fn contemArroba(s: []const u8) bool {
for (s) |c| {
if (c == '@') return true;
}
return false;
}
4. Retry Pattern
const std = @import("std");
const ErroRede = error{ Timeout, ConexaoRecusada };
fn enviarComRetry(
dados: []const u8,
maxTentativas: u32,
) ErroRede!void {
var tentativa: u32 = 0;
while (tentativa < maxTentativas) : (tentativa += 1) {
enviar(dados) catch |err| {
if (tentativa == maxTentativas - 1) return err;
std.log.warn("Tentativa {d} falhou: {}. Retentando...", .{
tentativa + 1, err
});
std.time.sleep(1 * std.time.ns_per_s);
continue;
};
return; // Sucesso
}
}
fn enviar(dados: []const u8) ErroRede!void {
_ = dados;
// ...
}
Comparação com Outras Linguagens
Zig vs Exceptions (Java/Python/C++)
Exceptions:
# Python
def dividir(a, b):
if b == 0:
raise ZeroDivisionError("Divisão por zero")
return a / b
try:
resultado = dividir(10, 0)
except ZeroDivisionError as e:
print(f"Erro: {e}")
Zig:
fn dividir(a: i32, b: i32) error{DivisaoPorZero}!i32 {
if (b == 0) return error.DivisaoPorZero;
return @divTrunc(a, b);
}
const resultado = dividir(10, 0) catch |err| {
std.debug.print("Erro: {}\n", .{err});
0
};
Vantagens do Zig:
- Nenhum overhead em caso de sucesso
- Erros são parte do tipo (type-safe)
- Não há unwinding de stack
- Performance previsível
Zig vs Result (Rust)
Rust:
fn dividir(a: i32, b: i32) -> Result<i32, Erro> {
if b == 0 { return Err(Erro::DivisaoPorZero); }
Ok(a / b)
}
let resultado = dividir(10, 2)?; // ? propaga erro
Zig:
fn dividir(a: i32, b: i32) !i32 {
if (b == 0) return error.DivisaoPorZero;
return @divTrunc(a, b);
}
const resultado = try dividir(10, 2); // try propaga erro
Diferenças:
- Rust usa
Result<T,E>(enum genérico) - Zig usa error unions integrados (
!T) - Ambos são zero-cost e type-safe
- Zig é mais sintaticamente leve
Zig vs Go
Go:
func dividir(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divisão por zero")
}
return a / b, nil
}
resultado, err := dividir(10, 0)
if err != nil {
log.Println(err)
}
Zig:
fn dividir(a: i32, b: i32) !i32 {
if (b == 0) return error.DivisaoPorZero;
return @divTrunc(a, b);
}
const resultado = dividir(10, 0) catch |err| {
std.log.err("{}", .{err});
0
};
Vantagens do Zig:
- Erros são verificados em compile-time (não pode ignorar)
tryé mais conciso queif err != nil- Não precisa retornar múltiplos valores
Melhores Práticas
1. Seja Específico nos Error Sets
// ✅ Bom: error set específico
const ErroParse = error{
NumeroInvalido,
Overflow,
};
// ❌ Ruim: error set genérico demais
fn parseNumero() anyerror!i32 { // Evite anyerror quando possível
// ...
}
2. Documente Erros Possíveis
/// Parseia uma string para i32.
/// Retorna error.NumeroInvalido se a string não for um número.
/// Retorna error.Overflow se o número for muito grande.
fn parseInt(texto: []const u8) !i32 {
// ...
}
3. Use errdefer para Cleanup
fn alocarRecursos() !Recursos {
const a = try alocarA();
errdefer liberarA(a);
const b = try alocarB();
errdefer liberarB(b);
return Recursos{ .a = a, .b = b };
}
4. Não Ignore Erros Silenciosamente
// ❌ Ruim: erro ignorado
_ = funcaoQuePodeFalhar();
// ✅ Bom: trate ou propague
funcaoQuePodeFalhar() catch |err| {
std.log.err("Falha: {}", .{err});
};
// Ou: propague explicitamente
try funcaoQuePodeFalhar();
5. Use orelse para Optionals, catch para Errors
// ✅ Correto
const valor = talvezValor orelse 0; // optional
const resultado = podeFalhar() catch 0; // error union
// ❌ Não funciona
// const x = podeFalhar() orelse 0; // ERRO: orelse é para ?T, não !T
6. Considere unreachable Apenas Quando Certeza Absoluta
// ✅ Seguro: pós-condições verificadas
const resultado = parseInt("42") catch unreachable; // Literal conhecido
// ❌ Perigoso: entrada do usuário
const entrada = lerLinha();
const num = parseInt(entrada) catch unreachable; // Pode falhar!
FAQ
Qual a diferença entre try e catch?
try: Propaga o erro para o chamador (early return)catch: Trata o erro localmente
// try: propaga
const x = try podeFalhar(); // retorna erro se falhar
// catch: trata aqui
const x = podeFalhar() catch 0; // usa 0 se falhar
Como criar um error set vazio?
const SemErro = error{}; // Nenhum erro possível
Isso é útil para generic code.
Posso converter entre error sets?
const ErroPequeno = error{ A, B };
const ErroGrande = error{ A, B, C, D };
// ErroPequeno converte para ErroGrande automaticamente
fn funcao() ErroPequeno!void {
return error.A;
}
fn outra() ErroGrande!void {
return funcao(); // OK: ErroPequeno ⊆ ErroGrande
}
Como verificar se um valor é erro?
const resultado = podeFalhar();
if (resultado) |valor| {
// é valor
} else |err| {
// é erro
}
O que acontece se eu não tratar um erro?
O compilador Zig rejeita código que ignora error unions:
const x = podeFalhar(); // ERRO: error union não tratado!
// Correto:
const x = try podeFalhar(); // propaga
const x = podeFalhar() catch 0; // trata
_ = podeFalhar() catch |e| std.log.err("{}", .{e}); // explícito
Como retornar múltiplos erros possíveis?
// Use || para unir error sets
fn operacao() (error{ A, B, C })!void {
// pode retornar A, B ou C
}
// Ou use anyerror
fn operacaoGenerica() anyerror!void {
// pode retornar qualquer erro
}
anyerror tem custo?
Sim. anyerror usa mais memória (pode ser u16 ou u32) comparado a error sets específicos que podem ser u8. Prefira error sets específicos quando possível.
Próximos Passos
Agora que você domina tratamento de erros em Zig, continue seu aprendizado:
Conteúdo Relacionado
- Structs, Enums e Unions em Zig — Complemente com tipos compostos
- Gerenciamento de Memória em Zig — Aprenda sobre allocators e error handling com memória
- Parsing JSON em Zig — Veja error handling na prática com parsing
- Criando uma CLI em Zig — Aplique tratamento de erros em aplicações reais
- Zig Build System — Configure testes e builds que aproveitam o sistema de erros do Zig
- Zig vs Rust — Compare abordagens de error handling
Pratique
- Implemente um parser com error handling completo
- Crie uma biblioteca com error sets bem definidos
- Refatore código usando
errdeferpara garantir cleanup
Recursos Adicionais
Resumo
| Conceito | Sintaxe | Uso |
|---|---|---|
| Error Set | error{ A, B } | Definir conjunto de erros |
| Error Union | E!T ou !T | “T ou erro” |
| Propagação | try expr | Retorna erro automaticamente |
| Tratamento | expr catch | Lida com erro localmente |
| Optional | ?T | “T ou null” |
| Fallback | expr orelse | Valor padrão para optional |
| Cleanup | errdefer | Executa só se der erro |
O sistema de erros de Zig é uma das features mais poderosas da linguagem. Ao tornar erros parte do sistema de tipos, Zig garante que falhas sejam tratadas explicitamente — sem exceptions caras, sem Result types verbosos, apenas código limpo e type-safe.
Escrito por Camila para ZigLang Brasil. Última atualização: 10 de fevereiro de 2026.
Achou algum erro? Tem sugestões? Contribua no GitHub