Tratamento de Erros em Zig: Guia Completo

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

  1. Visão Geral do Sistema de Erros
  2. Error Sets
  3. Error Unions
  4. Propagando Erros com try
  5. Tratando Erros com catch
  6. Valores Padrão com orelse
  7. errdefer — Defer Condicional
  8. Erros em Métodos e Structs
  9. Error Inference
  10. Padrões Comuns
  11. Comparação com Outras Linguagens
  12. Melhores Práticas
  13. FAQ
  14. 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:

  1. Erros são explícitos — toda função que pode falhar declara isso no tipo de retorno
  2. Não há exceções — não existe mecanismo de unwinding ou catch-all
  3. Zero overhead — tratamento de erros não custa em runtime quando não falha
  4. Composição simples — é fácil combinar e propagar erros

Comparativo Rápido

AspectoExceptions (Java/C++)Result<T,E> (Rust)Zig Error Unions
Erro é…Exception lançadaEnum Ok/ErrValor especial
Declaraçãothrows (opcional)Result<T,E>!T ou E!T
PropagaçãoAutomaticamente? operatortry keyword
OverheadAlto (stack unwinding)BaixoZero
Type safetyRuntimeCompile timeCompile 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 que if 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

  1. Structs, Enums e Unions em Zig — Complemente com tipos compostos
  2. Gerenciamento de Memória em Zig — Aprenda sobre allocators e error handling com memória
  3. Parsing JSON em Zig — Veja error handling na prática com parsing
  4. Criando uma CLI em Zig — Aplique tratamento de erros em aplicações reais
  5. Zig Build System — Configure testes e builds que aproveitam o sistema de erros do Zig
  6. 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 errdefer para garantir cleanup

Recursos Adicionais


Resumo

ConceitoSintaxeUso
Error Seterror{ A, B }Definir conjunto de erros
Error UnionE!T ou !T“T ou erro”
Propagaçãotry exprRetorna erro automaticamente
Tratamentoexpr catchLida com erro localmente
Optional?T“T ou null”
Fallbackexpr orelseValor padrão para optional
CleanuperrdeferExecuta 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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.