Perguntas de Entrevista sobre Error Handling em Zig
O sistema de tratamento de erros de Zig é um dos seus diferenciais mais importantes. Diferente de exceções (Java, Python, C++), códigos de retorno (C), ou Result types (Rust), Zig usa error unions que combinam o melhor dessas abordagens. Entrevistadores testam este tema extensivamente porque demonstra compreensão profunda da filosofia da linguagem.
Conceitos Fundamentais
Explique o que são error unions em Zig.
Um error union (!T) é um tipo que pode conter um valor de sucesso de tipo T ou um valor de erro de um error set. É o mecanismo principal de tratamento de erros em Zig:
fn dividir(a: f64, b: f64) !f64 {
if (b == 0) return error.DivisaoPorZero;
return a / b;
}
O tipo retornado é error{DivisaoPorZero}!f64 — ou sucesso com f64, ou o erro DivisaoPorZero.
Qual a diferença entre try, catch e if para error handling?
try: Propaga o erro para o chamador. Equivalente a expr catch |err| return err:
const resultado = try dividir(10, 0); // propaga o erro
catch: Captura e trata o erro localmente:
const resultado = dividir(10, 0) catch |err| {
std.log.err("Erro: {}", .{err});
return err;
};
// Ou com valor default
const resultado = dividir(10, 0) catch 0;
if com error union:
if (dividir(10, 0)) |valor| {
// usar valor
} else |err| {
// tratar erro
}
O que são error sets e como eles se compõem?
Error sets são conjuntos de erros possíveis que uma função pode retornar:
const FileError = error{
NotFound,
PermissionDenied,
OutOfMemory,
};
const NetworkError = error{
ConnectionRefused,
Timeout,
OutOfMemory,
};
// Union de error sets
const AppError = FileError || NetworkError;
Zig usa interseção e união de error sets para determinar automaticamente quais erros uma função pode retornar. O compilador rastreia error sets em tempo de compilação.
Explique errdefer e quando usá-lo.
errdefer executa uma expressão apenas quando a função retorna um erro. É essencial para cleanup parcial:
fn inicializar(allocator: Allocator) !*Sistema {
const memoria = try allocator.alloc(u8, 1024);
errdefer allocator.free(memoria); // libera se falhar abaixo
const config = try carregarConfig();
errdefer config.deinit(); // limpa se falhar abaixo
const conexao = try conectarBD();
// Se tudo OK, caller é responsável pelo cleanup
return Sistema{ .memoria = memoria, .config = config, .conexao = conexao };
}
Sem errdefer, seria necessário try-catch manual para cada operação, tornando o código verboso e propenso a leaks.
Perguntas Intermediárias
Quando usar catch unreachable vs catch com tratamento?
catch unreachable indica que você tem certeza absoluta que o erro não pode ocorrer. O compilador gera um panic se o erro ocorrer em modo debug:
// OK: sabemos que o slice tem pelo menos 1 elemento
const primeiro = items[0]; // se items é empty, panic
// Em funções onde o error set é logicamente impossível
var list = std.ArrayList(u8).init(allocator);
list.appendAssumeCapacity(42); // precisa ter capacidade garantida
Use catch unreachable raramente e apenas quando puder provar logicamente que o erro é impossível. Na dúvida, trate o erro.
Como Zig trata erros de alocação de memória?
Alocação em Zig pode falhar (retornar error.OutOfMemory), e funções que alocam retornam error unions. Isso força tratamento explícito:
fn processarDados(allocator: Allocator, dados: []const u8) ![]u8 {
const resultado = try allocator.alloc(u8, dados.len * 2);
errdefer allocator.free(resultado);
// ... processar ...
return resultado;
}
Diferente de C onde malloc retorna NULL (facilmente ignorado) ou C++ onde new lança exceção (facilmente não capturada), Zig torna impossível ignorar falha de alocação.
Como converter entre error sets?
const SpecificError = error{NotFound, PermissionDenied};
const BroadError = error{NotFound, PermissionDenied, Timeout, OutOfMemory};
fn funcaoEspecifica() SpecificError!void {
return error.NotFound;
}
fn funcaoAmpla() BroadError!void {
// Funciona: SpecificError é subconjunto de BroadError
try funcaoEspecifica();
return error.Timeout;
}
Error sets de subconjuntos são automaticamente coercidos para superconjuntos. O inverso requer tratamento explícito.
Cenários Práticos
Implemente retry com backoff usando error handling de Zig.
fn comRetry(comptime maxTentativas: u32, operacao: anytype) !@TypeOf(operacao()).Payload {
var tentativa: u32 = 0;
while (tentativa < maxTentativas) : (tentativa += 1) {
if (operacao()) |resultado| {
return resultado;
} else |err| {
if (tentativa + 1 >= maxTentativas) return err;
std.time.sleep(std.time.ns_per_ms * (@as(u64, 100) << @intCast(tentativa)));
}
}
unreachable;
}
Como Zig se compara com outras linguagens no tratamento de erros?
| Aspecto | Zig | C | Rust | Go | Java |
|---|---|---|---|---|---|
| Mecanismo | Error unions | Códigos retorno | Result<T,E> | Múltiplos retornos | Exceções |
| Propagação | try | Manual | ? operator | if err != nil | throws |
| Ignorar erro | Impossível | Fácil | Possível com unwrap | Fácil com _ | Possível |
| Performance | Zero overhead | Zero overhead | Zero overhead | Low overhead | Stack unwinding |
| Cleanup | errdefer | Goto cleanup | Drop trait | defer | finally |
Boas Práticas
- Nunca ignore erros — trate ou propague explicitamente
- Use
errdeferpara cleanup — garanta liberação de recursos em caso de erro - Error sets específicos — defina error sets que comunicam claramente o que pode falhar
- Evite
catch unreachable— a menos que possa provar logicamente que o erro é impossível - Documente erros — use doc comments para explicar quando cada erro ocorre
Preparação Complementar
- Perguntas de memória — error handling e memória são intimamente ligados
- Perguntas básicas — fundamentos da linguagem
- Design patterns — padrões que envolvem error handling
- Desafios de código — pratique implementação
- Tutoriais para exemplos práticos