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
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento básico de Zig. Consulte a introdução ao Zig
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:
errdefer std.posix.close(socket)executaerrdefer config.deinit()executaerrdefer 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.