---
title: "Error Handling em Zig: Boas Práticas com try, catch e errdefer"
url: "https://ziglang.com.br/artigos/zig-error-handling-boas-praticas/"
markdown_url: "https://ziglang.com.br/artigos/zig-error-handling-boas-praticas.MD"
description: "Guia prático de error handling em Zig para produção: error unions, error sets, try, catch, errdefer, limpeza de recursos, APIs, logs e testes."
date: "2026-02-21"
author: ""
---

# Error Handling em Zig: Boas Práticas com try, catch e errdefer

Guia prático de error handling em Zig para produção: error unions, error sets, try, catch, errdefer, limpeza de recursos, APIs, logs e testes.


Error handling em Zig não é exceção escondida, código de retorno esquecido nem macro mágica. A linguagem transforma falhas em parte explícita da assinatura: se uma função pode falhar, o tipo mostra isso. Esse detalhe muda a arquitetura de APIs, testes, logs, cleanup de recursos e até o desenho de serviços em produção.

A prática, porém, exige disciplina. É fácil criar `error{Tudo}` genérico demais, perder contexto ao converter erros, usar `catch unreachable` sem justificativa ou fazer retry em falhas que deveriam abortar imediatamente. Este guia atualiza as boas práticas de **tratamento de erros em Zig** para projetos reais em 2026: `try`, `catch`, error unions, error sets, `errdefer`, mapeamento de erros de dependências, observabilidade e testes.

Se você está começando, leia também a [cheatsheet de error handling](/cheatsheets/error-handling/) e as [perguntas de entrevista sobre erros](/entrevistas/perguntas-error-handling-zig/). Para aplicar isso em serviços, combine com [configuração segura](/artigos/zig-configuracao-segura-segredos-env/), [servidor HTTP em produção](/artigos/zig-http-server-producao/), [observabilidade em Zig](/artigos/zig-observabilidade/) e [checklist de code review](/artigos/zig-code-review-checklist/).

## Resumo rápido: como pensar erros em Zig

| Situação | Padrão recomendado | Evite |
|---|---|---|
| Falha esperada que o chamador pode tratar | `error set` específico + error union | Booleano sem motivo da falha |
| Propagar falha sem adicionar contexto | `try chamada()` | `catch return err` redundante |
| Traduzir erro de dependência para domínio da aplicação | `catch return error.ConfigInvalida` | Vazar erro interno para toda a API |
| Alocar ou abrir recursos em etapas | `errdefer` ao lado de cada aquisição | Bloco de cleanup distante e incompleto |
| Erro impossível por invariante local | `catch unreachable` com comentário forte | `unreachable` para esconder dívida técnica |
| Erro em produção | log estruturado + status/ação clara | logar só `erro: {}` sem contexto |

A regra central: **erros devem ser explícitos na borda certa**. Dentro de uma camada pequena, preserve detalhes úteis. Na fronteira pública da API, converta para uma superfície estável que o usuário, o serviço HTTP ou o operador consiga entender.

## Error unions: falha faz parte do tipo

Uma função que retorna `ArquivoError![]const u8` devolve bytes ou um erro do conjunto `ArquivoError`. O chamador não consegue ignorar isso sem escrever uma decisão explícita.

```zig
const std = @import("std");

const ArquivoError = error{
    NaoEncontrado,
    PermissaoNegada,
    MuitoGrande,
    IOError,
};

fn lerArquivo(allocator: std.mem.Allocator, caminho: []const u8) ArquivoError![]u8 {
    const file = std.fs.cwd().openFile(caminho, .{}) catch |err| switch (err) {
        error.FileNotFound => return error.NaoEncontrado,
        error.AccessDenied => return error.PermissaoNegada,
        else => return error.IOError,
    };
    defer file.close();

    const stat = file.stat() catch return error.IOError;
    if (stat.size > 1024 * 1024) return error.MuitoGrande;

    return file.readToEndAlloc(allocator, stat.size) catch return error.IOError;
}
```

O detalhe importante é que o exemplo não exporta todos os erros possíveis de `std.fs` para o restante da aplicação. A função escolhe uma fronteira: quem chama precisa saber se o arquivo não existe, se falta permissão, se é grande demais ou se houve I/O genérico. O resto é detalhe de implementação.

## `try` para o caminho comum

Use `try` quando a função atual não tem nada útil a acrescentar ao erro. Isso mantém o caminho feliz legível e evita blocos `catch` decorativos.

```zig
fn carregarTemplate(allocator: std.mem.Allocator) ![]u8 {
    const dir = try std.fs.cwd().openDir("templates", .{});
    defer dir.close();

    return try dir.readFileAlloc(allocator, "email.html", 64 * 1024);
}
```

Não transforme todo `try` em `catch |err| return err;`. O resultado é mais ruído, não mais controle.

## `catch` quando existe uma decisão local

Use `catch` quando o erro muda o fluxo: fallback, conversão de erro, log, métrica, resposta HTTP, retry ou encerramento controlado.

```zig
const AppError = error{
    ConfigInvalida,
    BancoIndisponivel,
    CacheIndisponivel,
};

fn inicializarApp(allocator: std.mem.Allocator) AppError!App {
    const config = Config.load(allocator) catch |err| {
        std.log.err("configuração inválida: {}", .{err});
        return error.ConfigInvalida;
    };
    errdefer config.deinit();

    const db = conectarBanco(config.database_url) catch |err| {
        std.log.err("banco indisponível durante boot: {}", .{err});
        return error.BancoIndisponivel;
    };
    errdefer db.close();

    const cache = conectarCache(config.redis_url) catch {
        // Cache é útil, mas a aplicação pode subir sem ele.
        null;
    };

    return .{ .config = config, .db = db, .cache = cache };
}
```

Essa diferença entre erro fatal e degradação aceitável precisa estar no código. Em produção, a escolha afeta deploy, rollback, alerta e experiência do usuário.

## Error sets pequenos e semânticos

Um erro de domínio deve dizer o que a aplicação pode fazer, não apenas qual biblioteca falhou.

```zig
const CriarUsuarioError = error{
    EmailInvalido,
    UsuarioJaExiste,
    BancoIndisponivel,
    LimiteAtingido,
};

fn criarUsuario(input: CriarUsuarioInput) CriarUsuarioError!Usuario {
    if (!emailValido(input.email)) return error.EmailInvalido;

    inserirUsuario(input) catch |err| switch (err) {
        error.UniqueViolation => return error.UsuarioJaExiste,
        error.ConnectionRefused, error.Timeout => return error.BancoIndisponivel,
        error.TooManyConnections => return error.LimiteAtingido,
        else => return error.BancoIndisponivel,
    };

    return Usuario{ .email = input.email };
}
```

Esse padrão aparece em integrações com [SQLite, PostgreSQL e Redis em Zig](/artigos/zig-banco-dados-integracoes/): a camada de infraestrutura conhece `sqlite3`, `libpq` ou socket errors; a camada de produto conhece `UsuarioJaExiste`, `BancoIndisponivel` e `LimiteAtingido`.

## `errdefer`: cleanup perto da aquisição

`errdefer` executa apenas se o escopo sair com erro. Ele é uma das ferramentas mais importantes de Zig para código que aloca memória, abre arquivos, inicializa sockets ou monta estruturas em etapas.

```zig
const Servidor = struct {
    allocator: std.mem.Allocator,
    buffer: []u8,
    log_file: std.fs.File,
    listener: std.net.Server,

    pub fn init(allocator: std.mem.Allocator, addr: std.net.Address) !Servidor {
        const buffer = try allocator.alloc(u8, 16 * 1024);
        errdefer allocator.free(buffer);

        const log_file = try std.fs.cwd().createFile("server.log", .{ .truncate = false });
        errdefer log_file.close();

        var listener = try addr.listen(.{ .reuse_address = true });
        errdefer listener.deinit();

        return .{
            .allocator = allocator,
            .buffer = buffer,
            .log_file = log_file,
            .listener = listener,
        };
    }

    pub fn deinit(self: *Servidor) void {
        self.listener.deinit();
        self.log_file.close();
        self.allocator.free(self.buffer);
    }
};
```

O padrão é simples: **adquiriu recurso, escreva o cleanup imediatamente na linha seguinte**. Se uma etapa futura falhar, os recursos anteriores serão liberados na ordem inversa. Se tudo der certo, `errdefer` não roda e a responsabilidade passa para `deinit`.

## `defer` e `errdefer` juntos

Use `defer` para cleanup que sempre acontece dentro do escopo atual. Use `errdefer` quando o recurso só deve ser limpo se a construção falhar antes da posse ser transferida.

```zig
fn copiarArquivo(allocator: std.mem.Allocator, origem: []const u8, destino: []const u8) !void {
    const in = try std.fs.cwd().openFile(origem, .{});
    defer in.close();

    const tmp_path = try std.fmt.allocPrint(allocator, "{s}.tmp", .{destino});
    defer allocator.free(tmp_path);

    const out = try std.fs.cwd().createFile(tmp_path, .{});
    defer out.close();

    var buffer = std.ArrayList(u8).init(allocator);
    defer buffer.deinit();

    var tmp: [4096]u8 = undefined;
    while (true) {
        const n = try in.read(&tmp);
        if (n == 0) break;
        try buffer.appendSlice(tmp[0..n]);
    }

    try out.writeAll(buffer.items);
    try std.fs.cwd().rename(tmp_path, destino);
}
```

Em funções de construção (`init`), `errdefer` costuma dominar. Em funções operacionais que abrem e fecham tudo no mesmo escopo, `defer` é suficiente.

## Fallbacks: só quando o default é seguro

`catch` com default é útil para configuração opcional, mas perigoso para dados críticos.

```zig
fn obterPorta() u16 {
    const raw = std.posix.getenv("PORT") orelse return 8080;
    return std.fmt.parseInt(u16, raw, 10) catch 8080;
}

fn obterLimiteBody() usize {
    const raw = std.posix.getenv("MAX_BODY_BYTES") orelse return 1 * 1024 * 1024;
    return std.fmt.parseInt(usize, raw, 10) catch |err| {
        std.log.warn("MAX_BODY_BYTES inválido, usando default: {}", .{err});
        return 1 * 1024 * 1024;
    };
}
```

Para segredos, endpoints de banco e flags que mudam segurança, prefira falhar no boot. Um default silencioso em `DATABASE_URL` ou `JWT_SECRET` cria incidente, não resiliência.

## Retry com backoff: filtre erros transitórios

Retry só ajuda em falhas transitórias. Repetir erro de validação, permissão ou configuração aumenta latência e esconde a causa.

```zig
const ConnectError = error{
    Timeout,
    Recusado,
    DnsFalhou,
    ConfigInvalida,
};

fn transitorio(err: ConnectError) bool {
    return switch (err) {
        error.Timeout, error.Recusado, error.DnsFalhou => true,
        error.ConfigInvalida => false,
    };
}

fn conectarComRetry(endereco: []const u8) ConnectError!Conexao {
    var tentativa: u32 = 0;
    var delay_ms: u64 = 100;

    while (tentativa < 5) : (tentativa += 1) {
        return conectar(endereco) catch |err| {
            if (!transitorio(err)) return err;
            if (tentativa == 4) return err;
            std.log.warn("conexão falhou, retry em {d}ms: {}", .{ delay_ms, err });
            std.time.sleep(delay_ms * std.time.ns_per_ms);
            delay_ms *= 2;
            continue;
        };
    }

    unreachable;
}
```

Em serviços HTTP, acrescente jitter e limite global para não transformar uma queda parcial em tempestade de retries.

## Logs, métricas e status HTTP

Erros de Zig ficam melhores quando atravessam uma camada de observabilidade consistente. Um handler HTTP pode mapear erro de domínio para status, log e métrica sem vazar detalhe interno.

```zig
fn responderErro(err: CriarUsuarioError) HttpResponse {
    return switch (err) {
        error.EmailInvalido => .{ .status = 400, .body = "email inválido" },
        error.UsuarioJaExiste => .{ .status = 409, .body = "usuário já existe" },
        error.LimiteAtingido => .{ .status = 429, .body = "tente novamente mais tarde" },
        error.BancoIndisponivel => .{ .status = 503, .body = "serviço indisponível" },
    };
}
```

O mesmo switch pode incrementar contadores como `http_errors_total{kind="usuario_ja_existe"}` e registrar `trace_id`. Não coloque SQL, path absoluto, token ou payload sensível na resposta. Para isso, siga as práticas de [configuração segura e logs sem segredo](/artigos/zig-configuracao-segura-segredos-env/).

## Teste o caminho de erro

Código Zig costuma testar o caminho feliz com facilidade. O diferencial é testar falhas esperadas: allocator que falha, arquivo ausente, JSON inválido, conexão recusada e recursos parcialmente inicializados.

```zig
test "parser rejeita JSON vazio" {
    try std.testing.expectError(error.BodyVazio, parseBody(""));
}

test "init libera memória quando segunda etapa falha" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak detectado");

    const allocator = gpa.allocator();
    try std.testing.expectError(error.ConfigInvalida, initComConfigRuim(allocator));
}
```

Combine isso com a [cheatsheet de testing](/cheatsheets/testing/) e com o [checklist de code review](/artigos/zig-code-review-checklist/): todo PR que mexe em inicialização, memória ou I/O deve provar pelo menos um caminho de falha.

## Anti-padrões comuns

### Ignorar erro silenciosamente

```zig
// Ruim: desaparece com a falha.
_ = enviarMetrica() catch {};

// Melhor: documenta a decisão operacional.
enviarMetrica() catch |err| {
    std.log.warn("falha ao enviar métrica não crítica: {}", .{err});
};
```

Ignorar erro pode ser correto para telemetria best-effort, mas o motivo precisa ficar claro.

### `catch unreachable` por preguiça

```zig
// Ruim: se o arquivo sumir em produção, o processo panica.
const file = std.fs.cwd().openFile("config.json", .{}) catch unreachable;
```

Use `unreachable` apenas quando a invariante é realmente local e provada. Em inicialização, erro de arquivo/configuração deve virar falha clara de boot.

### Error set grande demais

Um `AppError` usado por todo o monólito parece simples, mas vira acoplamento. Prefira error sets por fronteira: `ConfigError`, `DbError`, `HttpHandlerError`, `JobError`. Na borda externa, converta para uma superfície menor.

### Log duplicado em todas as camadas

Se cada função faz `std.log.err` e repropaga, uma falha vira dez linhas iguais. Logue no ponto que tem contexto operacional: handler, worker, boot step ou boundary de integração.

## Checklist para produção

Antes de publicar uma biblioteca, CLI ou serviço Zig, revise:

- error sets têm nomes semânticos e pequenos?
- erros de dependências são convertidos na fronteira certa?
- `errdefer` fica imediatamente depois de cada aquisição de recurso?
- caminhos de falha têm testes com `expectError`?
- defaults são seguros e documentados?
- retries filtram apenas erros transitórios?
- respostas HTTP não vazam detalhe interno?
- logs têm contexto suficiente sem incluir segredo?
- `catch unreachable` tem justificativa real?
- code review verifica memória, cleanup e erro juntos?

## Conclusão

O sistema de erros de Zig é poderoso porque obriga o projeto a decidir. `try` mantém o caminho comum limpo; `catch` explicita fallback e conversão; `errdefer` evita vazamento em construção parcial; error sets pequenos transformam falhas técnicas em decisões de produto e operação.

Para comparar abordagens, veja como <a href="https://rustlang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust</a> usa `Result` e pattern matching, e como <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go</a> adota retorno múltiplo com `if err != nil`. Zig fica no meio: explícito como Go, mas com tipos de erro que o compilador entende.

## Conteúdo relacionado

- [Cheatsheet de Error Handling](/cheatsheets/error-handling/) — referência rápida de `try`, `catch`, `errdefer` e error sets
- [Tratamento de Erros em Zig](/tutoriais/tratamento-de-erros-em-zig/) — tutorial passo a passo
- [Checklist de Code Review em Zig](/artigos/zig-code-review-checklist/) — revisão de memória, erros, segurança e testes
- [Configuração Segura em Zig](/artigos/zig-configuracao-segura-segredos-env/) — falha de boot, env vars e logs sem segredo
- [Observabilidade em Zig](/artigos/zig-observabilidade/) — logs, métricas, traces e alertas
- [Perguntas de Error Handling](/entrevistas/perguntas-error-handling-zig/) — preparação para entrevistas
