Erros como valores: Result, neverthrow e por que `throw` quebra
- ⬜⏳ Async/await sem pegadinha: promises, AbortController e cancelamento(TypeScript Profissional)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
O problema com throw
A função parseJson(s: string): User parece limpa — mas é mentira. Ela pode lançar SyntaxError. Quem chama não tem como saber pela assinatura. Se você esqueceu o try/catch, o erro propaga silenciosamente até algum ancestor que pode ou não tratá-lo.
Em TS, throw é invisível ao sistema de tipos. Ao contrário de Java (checked exceptions) ou Rust (Result), a assinatura não obriga ninguém a tratar.
Result<T, E>: o padrão
📋 Como sinalizar falha nesta função
Falha prevista — rede caiu, validação recusou, não autorizado — pertence à assinatura, porque quem chama precisa decidir o que fazer. Invariante violado não pertence: continuar em estado corrompido é pior que quebrar, e o compilador não deveria pedir tratamento para algo que nunca deveria acontecer.
Alt: Lançar para tudo — A assinatura fica idêntica à de uma função infalível, e ninguém é avisado
Alt: Retornar nulo — Perde o motivo: "não achei" e "o banco caiu" viram o mesmo valor
Alt: Retorno explícito até para bug — Polui todas as assinaturas com um caso que ninguém trata
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseJson<T>(raw: string): Result<T, SyntaxError> {
try {
return { ok: true, value: JSON.parse(raw) };
} catch (e) {
return { ok: false, error: e as SyntaxError };
}
}
// Uso — impossível esquecer de tratar
const r = parseJson<User>(raw);
if (r.ok) console.log(r.value.name);
else handleError(r.error);A assinatura mostra que pode falhar. O TS força você a narrowing. Impossível esquecer.
neverthrow: biblioteca popular
import { ok, err, Result, ResultAsync } from 'neverthrow';
function parse(raw: string): Result<User, Error> {
try { return ok(JSON.parse(raw) as User); }
catch (e) { return err(e as Error); }
}
// Railway: .map aplica transformação só se ok
const r = parse(raw)
.map(u => u.email)
.map(email => email.toLowerCase())
.andThen(email => validateEmail(email)); // andThen = flatMap
if (r.isErr()) handleError(r.error);
else console.log(r.value);Por que `throw` é descrito como invisível ao sistema de tipos do TypeScript?
Quando throw é aceitável
- Invariantes violados: "isso nunca deveria acontecer" — throw crash early. Ex: switch default impossível.
- Bug de programação: null que foi garantido não-null, mas apareceu. Melhor crash do que continuar em estado corrompido.
- Boundaries externos onde a lib já lança: , . Aí você envolve uma vez num try/catch e retorna Result.
Erros esperados (rede caiu, validação falhou, não autorizado) → Result. Erros impossíveis (assertion, bug) → throw.
Por que FFV Academy segue essa filosofia
Este site usa padrão Result em importState (retorna { ok, error }), safeParseJSON, setUser. Boundaries de input — onde falha é esperada e precisa ser tratada com mensagem ao usuário. Nenhum throw silencioso vaza pra UI.
Perguntas frequentes
❓ Por que tratar erro como valor de retorno?
❓ Quando lançar exceção ainda é o certo?
❓ Isso não deixa o código verboso?
Fixando
Qual é o critério para escolher entre `Result` e `throw`?
Uma biblioteca externa lança exceção. Como isso se encaixa num código baseado em `Result`?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…