Perguntas de Entrevista sobre Error Handling em Zig

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?

AspectoZigCRustGoJava
MecanismoError unionsCódigos retornoResult<T,E>Múltiplos retornosExceções
PropagaçãotryManual? operatorif err != nilthrows
Ignorar erroImpossívelFácilPossível com unwrapFácil com _Possível
PerformanceZero overheadZero overheadZero overheadLow overheadStack unwinding
CleanuperrdeferGoto cleanupDrop traitdeferfinally

Boas Práticas

  1. Nunca ignore erros — trate ou propague explicitamente
  2. Use errdefer para cleanup — garanta liberação de recursos em caso de erro
  3. Error sets específicos — defina error sets que comunicam claramente o que pode falhar
  4. Evite catch unreachable — a menos que possa provar logicamente que o erro é impossível
  5. Documente erros — use doc comments para explicar quando cada erro ocorre

Preparação Complementar

Continue aprendendo Zig

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