Capítulo 04 8 min de leitura

Erros como valores

Em Python, uma função que falha lança uma exceção — um segundo caminho de retorno oculto que interrompe o fluxo normal até que algo a capture. Em Glide, uma função que falha retorna um valor especial que o chamador precisa examinar. Os modos de falha vivem na assinatura do tipo. Nada fica escondido.

Isso parece mais trabalhoso, e de início é — mas a recompensa é que você não consegue ignorar um erro por acidente, e todo lugar onde as coisas podem dar errado é visível direto no código-fonte.

Os dois tipos em formato de falha

Existem dois tipos que codificam "a coisa pode não estar lá":

  • `!T` — leia "bang T," significando "um T, ou uma mensagem de erro". Construído com ok(value) ou err("message").
  • `?T` — leia "maybe T," significando "um T, ou nada". Construído com some(value) ou none().

Use !T quando algo falhou por um motivo que vale explicar. Use ?T quando simplesmente não há valor a retornar, e você não precisa de um motivo.

fn parse_int(s: string) -> !i32 {
    if s.eq("") { return err("empty input"); }
    if !all_digits(s) { return err("not a number"); }
    return ok(/* o valor analisado */ 42);
}

fn first_vowel(s: string) -> ?string {
    for let i: i32 = 0; i < s.len(); i = i + 1 {
        if is_vowel(s.at(i).to_int()) {
            return some(s.substring(i, i + 1));
        }
    }
    return none();
}

Lendo o valor de dentro

Os dois tipos têm dois campos que você pode inspecionar: uma flag e um payload.

Para !T:

  • .ok é true quando a operação teve sucesso
  • .val contém o valor (só faz sentido quando .ok é true)
  • .err contém a mensagem (só faz sentido quando .ok é false)
let r: !i32 = parse_int("42");
if r.ok {
    println!("parsed:", r.val);
} else {
    println!("error:", r.err);
}

Para ?T:

  • .has é true quando há um valor
  • .val o contém
let v: ?string = first_vowel("hello");
if v.has {
    println!("got:", v.val);
} else {
    println!("no vowel");
}

O operador ?

Verificar .ok manualmente após cada chamada que pode falhar fica tedioso rápido:

fn pipeline(s: string) -> !i32 {
    let a: !i32 = parse_int(s);
    if !a.ok { return err(a.err); }   // interrompe
    let b: !i32 = parse_int(/* outra coisa */);
    if !b.ok { return err(b.err); }   // interrompe de novo
    return ok(a.val + b.val);
}

O operador ? colapsa esse padrão em um único caractere. Coloque-o logo após um valor !T:

fn pipeline(s: string) -> !i32 {
    let a: i32 = parse_int(s)?;    // err → retorna err; ok → desempacota para a
    let b: i32 = parse_int(s)?;
    return ok(a + b);
}

O que ? faz, mecanicamente:

  1. Se o .ok do valor for false, retorna esse erro da função que o envolve.
  2. Caso contrário, avalia para o .val.

Ele só funciona dentro de uma função que, ela mesma, retorna !T (ou ?T, que tem semântica análoga para none).

O operador ??: valores padrão

Às vezes você não se importa com o erro — você só quer um valor de fallback. ?? é o operador para isso:

let port: i32 = parse_int(env_get("PORT")) ?? 8080;

Se parse_int tiver sucesso, port recebe seu valor. Se falhar, port recebe 8080. Sem if, sem match, sem retorno antecipado.

?? também funciona com ?T:

let first: string = first_vowel(input) ?? "no vowels";

Construindo os valores

Os construtores são funções em letras minúsculas:

return ok(42);
return err("bad input");
return some("hi");
return none();

Todas as quatro são chamadas em minúsculas. none() não recebe argumento porque não há nada a envolver; as outras três recebem a coisa que envolvem.

defer e defer_err

Quando uma função abre um recurso (um arquivo, uma conexão, uma alocação) ela normalmente precisa fechá-lo antes de retornar — quer a função tenha tido sucesso ou não. Glide tem duas palavras-chave para isso.

`defer` agenda um código para ser executado quando a função que o envolve sair, não importa como (retorno normal, propagação por ?, return antecipado). É assim que você garante a limpeza:

fn process(path: string) -> !i32 {
    let f: *File = open(path)?;
    defer f.close();      // executa quando esta função retornar, ponto final

    let line: string = f.read_line()?;   // se isso falhar, f ainda será fechado
    return ok(line.len());
}

`defer_err` é a mesma ideia, mas só é acionado no caminho de erro — útil para ações compensatórias:

fn create_user(name: string) -> !User {
    let id: i64 = db_insert(name)?;
    defer_err db_delete(id);            // desfaz APENAS se algo abaixo falhar

    send_welcome_email(name)?;          // se isso falhar, o rollback é executado
    return ok(User { id: id, name: name });
}

Se send_welcome_email falhar, defer_err executa e a inserção parcial é desfeita. Se tudo der certo, defer_err é ignorado — o usuário permanece.

Comparado ao try/except do Python

Considere este Python:

python
def get_config(path):
    try:
        with open(path) as f:
            data = json.load(f)
        return data["port"]
    except FileNotFoundError:
        return 8080
    except json.JSONDecodeError as e:
        raise ValueError(f"bad JSON: {e}")

Em Glide, a mesma lógica fica assim:

import stdlib::fs::*;
import stdlib::json::*;

fn get_config(path: string) -> !i32 {
    let raw: string = fs_read_or_default(path, "");    // arquivo ausente → ""
    if raw.eq("") { return ok(8080); }                 // porta padrão
    let cfg: *JsonValue = JsonValue::parse(raw);
    let port: i32 = cfg.get_int("port")?;              // chave ausente/não inteira → propaga
    return ok(port);
}

Observe:

  • fs_read_or_default colapsa o caso de "arquivo ausente" para um fallback inline — sem try/except.
  • cfg.get_int("port") retorna !i32, então o ? propaga um erro de "chave inexistente" / "não é um inteiro" — o chamador o vê como !i32 com essa mensagem.
  • O tipo de retorno !i32 anuncia que esta função pode falhar. Quem a chama sabe imediatamente.

Quando erros são verdadeiramente irrecuperáveis

Para estados genuinamente impossíveis ("o array está vazio aqui, o que meus invariantes proíbem"), use panic! ou assert! — eles imprimem uma mensagem com a localização no código-fonte e abortam o processo:

let v: *Vector<i32> = Vector::new();
v.push_all!(1, 2, 3);
if v.len() == 0 {
    panic!("this can't happen — I just pushed three elements");
}

Mas reserve isso para casos genuinamente "impossíveis". Qualquer coisa que um usuário ou a rede possa desencadear deve ser !T e propagar normalmente.

Recapitulando

Para onde ir agora

Você já sabe escrever funções que falham com segurança. O próximo capítulo — Modelo de memória — explica a outra metade da história de segurança do Glide: como a linguagem rastreia quem possui cada pedaço de memória sem um coletor de lixo, e sem anotações de tempo de vida como as do Rust.