Error Union em Zig — O que é e Como Usar

Error Union em Zig — O que é e Como Usar

Definição

Um Error Union em Zig é um tipo composto representado pelo operador ! que pode conter ou um valor válido ou um erro. É o mecanismo fundamental de tratamento de erros da linguagem, substituindo exceções (como em Java/Python) e códigos de retorno (como em C).

A sintaxe ErrorSet!T significa “este valor é do tipo T ou é um erro do conjunto ErrorSet”. Quando o error set é omitido (!T), o compilador infere o conjunto de erros automaticamente.

Por que Error Unions Importam

  1. Erros são valores: Em Zig, erros são cidadãos de primeira classe no sistema de tipos — não são exceções invisíveis.
  2. Impossível ignorar erros: O compilador obriga você a lidar com o caso de erro explicitamente.
  3. Zero custo: Error unions são implementados de forma eficiente, sem overhead de exceções.
  4. Composição: Funções que retornam error unions se compõem naturalmente com try.

Exemplo Prático

Retornando Error Unions

const std = @import("std");

const MathError = error{
    DivisaoPorZero,
    Overflow,
};

fn dividir(a: i32, b: i32) MathError!i32 {
    if (b == 0) return MathError.DivisaoPorZero;
    return @divTrunc(a, b);
}

pub fn main() void {
    // Tratando o erro explicitamente
    const resultado = dividir(10, 3) catch |err| {
        std.debug.print("Erro: {}\n", .{err});
        return;
    };
    std.debug.print("10 / 3 = {}\n", .{resultado});
}

Usando com try

fn processarDados() !void {
    const valor = try dividir(100, 5);  // Propaga erro automaticamente
    std.debug.print("Resultado: {}\n", .{valor});
}

try é equivalente a:

const valor = dividir(100, 5) catch |err| return err;

Usando com if

fn exemploIf() void {
    if (dividir(10, 0)) |valor| {
        std.debug.print("Sucesso: {}\n", .{valor});
    } else |err| {
        std.debug.print("Erro: {}\n", .{err});
    }
}

Error Union com tipo inferido

// O compilador infere o error set automaticamente
fn lerArquivo(caminho: []const u8) ![]const u8 {
    const arquivo = try std.fs.cwd().openFile(caminho, .{});
    defer arquivo.close();
    return try arquivo.readToEndAlloc(std.heap.page_allocator, 1024 * 1024);
}

Anatomia do Tipo

ErrorSet ! PayloadType
             
             └── Tipo do valor em caso de sucesso
   └── Conjunto de possíveis erros

Exemplos:
  error{OutOfMemory}![]u8      Pode ser OutOfMemory ou um slice de bytes
  anyerror!void                Qualquer erro ou void (sem valor)
  !u32                         Error set inferido, valor u32

Combinando Error Sets

Quando uma função chama outras que retornam diferentes error sets, o compilador infere a união automaticamente:

const IoError = error{ DiskFull, PermissionDenied };
const ParseError = error{ InvalidFormat, UnexpectedEof };

// O compilador infere o tipo como IoError || ParseError
fn lerEProcessar(caminho: []const u8) !u32 {
    const conteudo = try lerArquivo(caminho);   // retorna IoError![]u8
    const numero = try parsear(conteudo);        // retorna ParseError!u32
    return numero;
}

Você também pode combinar error sets explicitamente com ||:

const ErrosCombinados = IoError || ParseError;

fn funcao() ErrosCombinados!void {
    // ...
}

Error Union vs Outros Mecanismos de Erro

MecanismoLinguagemPode ignorar?OverheadInformação
Error Union !TZigNão (compilador força)ZeroError set tipado
Result<T,E>RustNão (com warnings)ZeroTipo genérico
ExceçõesJava, PythonSim (silencioso)Alto (stack unwinding)Classe de exceção
Código de retorno intCSim (fácil de esquecer)ZeroApenas um inteiro
errno globalCSimZeroCódigo global mágico

A principal vantagem do error union sobre exceções é o custo zero: não há stack unwinding, não há heap allocation para o objeto de exceção, e o compilador garante que todos os erros sejam tratados.

Boas Práticas

  • Prefira error sets específicos a anyerror: Error sets nomeados documentam o contrato da função e permitem que o compilador verifique exaustividade em switches de erro.
  • Use errdefer para cleanup em caso de falha: Quando a função aloca recursos antes de um possível erro, errdefer garante que o cleanup ocorra apenas em caso de falha (ao contrário de defer, que sempre executa).
  • Documente erros possíveis: Mesmo que o compilador infira o error set, listar os erros possíveis em comentários ajuda quem usa a API.
  • Propague com try, não com catch unreachable: Se não souber tratar o erro, propague-o. catch unreachable é uma bomba-relógio se a premissa estiver errada.

Armadilhas Comuns

  • Usar anyerror desnecessariamente: Prefira error sets específicos. anyerror desabilita otimizações do compilador e dificulta a documentação da API.
  • Ignorar erros com catch unreachable: Só use quando tiver certeza absoluta de que o erro não pode ocorrer. Caso contrário, isso causará um panic em runtime.
  • Confundir !T com ?T: Error union (!T) carrega informação de erro; optional (?T) apenas indica presença/ausência de valor.
  • Não propagar erros: Se sua função não sabe lidar com o erro, propague-o com try em vez de engolir silenciosamente.

Termos Relacionados

  • Error Set — Conjuntos de erros nomeados
  • Try — Operador de propagação de erros
  • Catch — Operador para capturar erros
  • Optional — Tipo que pode ser null
  • Errdefer — Defer condicional para erros

Tutoriais Relacionados

Continue aprendendo Zig

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