Padrões Errdefer para Cleanup em Zig

Introdução

errdefer é uma das features mais elegantes de Zig. Diferente de defer (que executa sempre), errdefer executa apenas quando a função retorna um erro. Isso permite escrever código de inicialização que limpa recursos parcialmente alocados em caso de falha, sem precisar de blocos try/finally ou goto cleanup como em C.

Para error handling geral, veja Padrões Try/Catch e Error Sets Customizados.

Pré-requisitos

Padrão Básico: Alocação com Cleanup

const std = @import("std");

fn criarBuffer(allocator: std.mem.Allocator, tamanho: usize) ![]u8 {
    const buffer = try allocator.alloc(u8, tamanho);
    errdefer allocator.free(buffer); // Libera SÓ se retornar erro

    // Se esta operação falhar, buffer é liberado automaticamente
    try preencherBuffer(buffer);

    return buffer; // Sucesso: errdefer NÃO executa
}

Sem errdefer, teríamos que fazer:

// SEM errdefer (como seria em C)
fn criarBuffer(allocator: std.mem.Allocator, tamanho: usize) ![]u8 {
    const buffer = try allocator.alloc(u8, tamanho);

    preencherBuffer(buffer) catch |err| {
        allocator.free(buffer); // Limpeza manual
        return err;
    };

    return buffer;
}

Inicialização em Múltiplos Passos

const Servidor = struct {
    socket: std.posix.socket_t,
    config: Config,
    buffer: []u8,
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator, porta: u16) !Servidor {
        // Passo 1: Alocar buffer
        const buffer = try allocator.alloc(u8, 8192);
        errdefer allocator.free(buffer);

        // Passo 2: Carregar config (se falhar, buffer é liberado)
        const config = try Config.carregar(allocator);
        errdefer config.deinit();

        // Passo 3: Abrir socket (se falhar, config e buffer são liberados)
        const socket = try abrirSocket(porta);
        errdefer std.posix.close(socket);

        // Passo 4: Bind (se falhar, tudo acima é liberado)
        try bindSocket(socket, porta);

        // Sucesso: NENHUM errdefer executa
        return .{
            .socket = socket,
            .config = config,
            .buffer = buffer,
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *Servidor) void {
        std.posix.close(self.socket);
        self.config.deinit();
        self.allocator.free(self.buffer);
    }
};

Se o passo 4 (bindSocket) falhar:

  1. errdefer std.posix.close(socket) executa
  2. errdefer config.deinit() executa
  3. errdefer allocator.free(buffer) executa

A ordem de execução é reversa (LIFO), igual a defer.

Errdefer com Captura de Erro

fn processar(allocator: std.mem.Allocator) !Resultado {
    const dados = try allocator.alloc(u8, 1024);
    errdefer |err| {
        std.log.err("Falha ao processar: {}", .{err});
        allocator.free(dados);
    };

    try etapa1(dados);
    try etapa2(dados);
    return Resultado{ .dados = dados };
}

Padrão: Builder com Errdefer

const Pipeline = struct {
    etapas: std.ArrayList(Etapa),
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator) Pipeline {
        return .{
            .etapas = std.ArrayList(Etapa).init(allocator),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *Pipeline) void {
        for (self.etapas.items) |*etapa| {
            etapa.deinit();
        }
        self.etapas.deinit();
    }

    pub fn adicionarEtapa(self: *Pipeline, config: EtapaConfig) !void {
        var etapa = try Etapa.init(self.allocator, config);
        errdefer etapa.deinit(); // Se append falhar, a etapa é limpa

        try self.etapas.append(etapa);
    }
};

Padrão: Transação (Tudo ou Nada)

fn transferir(
    db: *Database,
    de: u32,
    para: u32,
    valor: f64,
) !void {
    try db.iniciarTransacao();
    errdefer db.rollback(); // Se QUALQUER passo falhar, rollback

    try db.debitar(de, valor);
    try db.creditar(para, valor);
    try db.registrarLog(de, para, valor);

    try db.commit(); // Sucesso: errdefer não executa
}

Errdefer em Loops

fn criarWorkers(allocator: std.mem.Allocator, n: usize) ![]Worker {
    const workers = try allocator.alloc(Worker, n);
    errdefer allocator.free(workers);

    var inicializados: usize = 0;
    errdefer {
        // Limpar workers já inicializados em caso de falha
        for (workers[0..inicializados]) |*w| {
            w.deinit();
        }
    };

    for (workers) |*w| {
        w.* = try Worker.init(allocator);
        inicializados += 1;
    }

    return workers;
}

Defer vs Errdefer: Quando Usar Cada

fn exemplo(allocator: std.mem.Allocator) !Resultado {
    // defer: SEMPRE executa (sucesso ou erro)
    // Usar para recursos que devem ser limpos em ambos os casos
    const arquivo = try std.fs.cwd().openFile("temp.txt", .{});
    defer arquivo.close(); // Sempre fechar o arquivo

    // errdefer: executa SÓ em erro
    // Usar para recursos que serão TRANSFERIDOS ao chamador em sucesso
    const dados = try allocator.alloc(u8, 1024);
    errdefer allocator.free(dados); // Liberar SÓ se falhar

    try arquivo.readAll(dados);

    return Resultado{ .dados = dados }; // Transfere ownership ao chamador
}

Testes

test "errdefer libera em caso de erro" {
    const allocator = std.testing.allocator;

    // Forçar erro para verificar que errdefer limpa corretamente
    const resultado = criarBuffer(allocator, 0);

    // Se errdefer funcionar corretamente, testing.allocator
    // não detectará vazamento de memória
    try std.testing.expectError(error.TamanhoInvalido, resultado);
}

Veja Testes com Allocator para testar que errdefer funciona corretamente.

Conclusão

errdefer é essencial para escrever código Zig robusto. Ele garante cleanup automático de recursos parcialmente alocados em caso de erro, sem a complexidade de goto cleanup (C) ou try/finally (Java/C#). Use defer para cleanup incondicional e errdefer para cleanup condicional ao erro.

Para mais sobre erros em Zig, veja Padrões Try/Catch, Error Sets Customizados e Error Logging.

Continue aprendendo Zig

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