Padrões Try/Catch de Erros em Zig

Introdução

Zig usa error unions em vez de exceções. Uma função que pode falhar retorna !T (error union), e o chamador deve tratar o erro com try (propagar) ou catch (tratar). Este é um dos aspectos mais elegantes de Zig.

Para error sets customizados, veja Error Sets Customizados. Para cleanup, consulte Padrões Errdefer. Para o tutorial completo, veja Tratamento de Erros em Zig.

Pré-requisitos

Try: Propagar Erros

const std = @import("std");

fn lerArquivo(caminho: []const u8) ![]u8 {
    // try propaga o erro para o chamador
    const arquivo = try std.fs.cwd().openFile(caminho, .{});
    defer arquivo.close();

    var buffer: [4096]u8 = undefined;
    const n = try arquivo.readAll(&buffer);
    return buffer[0..n];
}

fn processarConfig() !Config {
    const dados = try lerArquivo("config.txt");
    return try parsearConfig(dados);
}

try é equivalente a:

const resultado = funcao() catch |err| return err;

Catch: Tratar Erros

Catch com Handler

const valor = funcao() catch |err| {
    std.log.err("Falha: {}", .{err});
    return;
};

Catch com Valor Padrão

const porta = std.fmt.parseInt(u16, porta_str, 10) catch 8080;

Catch com Return

const arquivo = std.fs.cwd().openFile("dados.bin", .{}) catch |err| {
    std.debug.print("Erro ao abrir: {}\n", .{err});
    return err;
};
defer arquivo.close();

Switch em Erros

const MeuErro = error{
    ArquivoNaoEncontrado,
    PermissaoNegada,
    FormatoInvalido,
    Timeout,
};

fn processar(caminho: []const u8) MeuErro!Resultado {
    // ...
}

fn executar() void {
    const resultado = processar("dados.txt") catch |err| switch (err) {
        error.ArquivoNaoEncontrado => {
            std.debug.print("Arquivo não encontrado\n", .{});
            return;
        },
        error.PermissaoNegada => {
            std.debug.print("Sem permissão\n", .{});
            return;
        },
        error.FormatoInvalido => {
            std.debug.print("Formato inválido\n", .{});
            return;
        },
        error.Timeout => {
            std.debug.print("Timeout\n", .{});
            return;
        },
    };
    _ = resultado;
}

Compor Funções com Erros

fn pipeline(allocator: std.mem.Allocator, caminho: []const u8) !Resultado {
    const raw = try lerArquivo(allocator, caminho);
    defer allocator.free(raw);

    const parsed = try parsear(raw);
    const validado = try validar(parsed);
    const resultado = try transformar(allocator, validado);

    return resultado;
}

Cada try propaga o erro se a função falhar. O defer garante cleanup do raw independentemente de onde o erro ocorra.

Erros em Loops

fn processarTodos(allocator: std.mem.Allocator, caminhos: []const []const u8) !void {
    for (caminhos) |caminho| {
        processar(allocator, caminho) catch |err| {
            std.log.warn("Falha em {s}: {}", .{ caminho, err });
            continue; // Continuar com o próximo
        };
    }
}

// Ou coletar resultados e erros
fn processarComResultados(
    allocator: std.mem.Allocator,
    caminhos: []const []const u8,
) !struct { sucessos: usize, falhas: usize } {
    var sucessos: usize = 0;
    var falhas: usize = 0;

    for (caminhos) |caminho| {
        if (processar(allocator, caminho)) |_| {
            sucessos += 1;
        } else |_| {
            falhas += 1;
        }
    }

    return .{ .sucessos = sucessos, .falhas = falhas };
}

Retries

fn comRetry(comptime max_tentativas: usize, func: anytype, args: anytype) !@typeInfo(@TypeOf(@call(.auto, func, args))).error_union.payload {
    var tentativa: usize = 0;
    while (tentativa < max_tentativas) : (tentativa += 1) {
        if (@call(.auto, func, args)) |resultado| {
            return resultado;
        } else |err| {
            if (tentativa + 1 < max_tentativas) {
                std.time.sleep(std.time.ns_per_s * (tentativa + 1));
            } else {
                return err;
            }
        }
    }
    unreachable;
}

Error Union com Optional

// Função que pode retornar erro, valor, ou "não encontrado"
fn buscar(id: u32) !?Resultado {
    if (id == 0) return error.IdInvalido;  // Erro
    if (id > 1000) return null;             // Não encontrado
    return Resultado{ .id = id };           // Sucesso
}

// Tratar os três casos
if (buscar(42)) |maybe_resultado| {
    if (maybe_resultado) |resultado| {
        std.debug.print("Encontrado: {}\n", .{resultado.id});
    } else {
        std.debug.print("Não encontrado\n", .{});
    }
} else |err| {
    std.debug.print("Erro: {}\n", .{err});
}

Testes com Erros

test "função retorna erro esperado" {
    const resultado = dividir(10, 0);
    try std.testing.expectError(error.DivisaoPorZero, resultado);
}

test "função retorna valor correto" {
    const resultado = try dividir(10, 2);
    try std.testing.expectEqual(@as(f64, 5.0), resultado);
}

Veja Testes Unitários e Test Expectations.

Conclusão

O sistema de erros de Zig é simples e poderoso: try propaga, catch trata, e o compilador garante que todo erro é considerado. Sem exceções invisíveis, sem stack unwinding, sem overhead de runtime.

Para padrões relacionados, veja Error Sets Customizados, Padrões Errdefer e Error Logging.

Continue aprendendo Zig

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