3. Simplicidade e resultados: KISS, YAGNI, DRY e exceções como valores
Depois deste capítulo você consegue recusar um trecho de código, um documento ou uma verificação de que nenhuma entrega precisa, dizer qual entre KISS, YAGNI e DRY o recusa, e saber o momento em que uma versão compartilhada é devida: a segunda ocorrência concreta.
Você consegue escrever uma regra de negócio como uma função pura que devolve um Result (resultado), e distinguir uma exceção de uma recusa e de um erro em qualquer linguagem.
Você também consegue manter as exceções de uma biblioteca, e só elas, na fronteira, quer a chamada responda na hora ou depois, de modo que uma biblioteca nova mude um arquivo e um bug ainda chegue até você.
E consegue fazer o compilador recusar um caso que você esqueceu de tratar, com um Record ou uma checagem never, já que declarar um Result sozinho não faz isso.
O que cresceu no Ninjobs
No Ninjobs, o meu próprio produto, eu deixei a complexidade crescer, uma adição razoável por vez. Eu exigia a forma completa, em camadas, da arquitetura em toda feature, formulários triviais incluídos, então mostrar um campo em uma tela levava oito arquivos; toda entrega tinha que passar por 29 verificações que nunca tinham pegado um erro no produto. A culpa não era da arquitetura; KISS e YAGNI se perderam.
Fred Brooks dividiu a dificuldade do software em duas em 1987: a complexidade essencial, que pertence ao próprio problema, e a complexidade acidental, que as pessoas que constroem acrescentam pelo caminho.1 Ninguém consegue remover a primeira; a segunda é uma escolha, feita um arquivo por vez, e os três princípios deste capítulo são três maneiras de recusá-la. Um agente deixa mais barato errar essa escolha: ele escreve a camada, a opção e a terceira cópia em segundos, cada uma plausível, e cada uma vira código que alguém precisa ler, revisar e manter verdadeiro.
KISS: mantenha simples
"Keep it simple, stupid" (mantenha simples, estúpido) é atribuído a Kelly Johnson, o engenheiro aeronáutico que liderou a Skunk Works da Lockheed, cuja regra era que o que os seus engenheiros projetassem tinha de poder ser consertado em campo por um mecânico mediano com um punhado de ferramentas comuns. A regra é sobre a pessoa que vem depois: o projeto tem que ser simples o bastante para ela, sob pressão, sem o autor na sala, porque uma coisa simples é uma coisa que se entende e se conserta.
No código, a pessoa que vem depois é um colega, um agente em uma sessão nova, ou você daqui a seis meses. KISS pede a forma mais simples que faz o trabalho: uma função antes de uma classe, um valor antes de uma configuração, um arquivo antes de três. Os exemplos de código deste livro foram escritos para ele, em TypeScript, curtos o bastante para caber na cabeça; as ideias valem em qualquer linguagem. Nesta edição, os nomes do código estão em português por uma escolha didática, não como recomendação: cabe à equipe decidir o que é melhor para o projeto e para as pessoas dele. A data de devolução de um empréstimo na biblioteca de empréstimos, o exemplo da Parte I, é 21 dias depois do empréstimo:
const DIAS_DE_EMPRESTIMO = 21;
function dataDeDevolucao(hoje: string): string {
const devolucao = new Date(hoje);
devolucao.setUTCDate(devolucao.getUTCDate() + DIAS_DE_EMPRESTIMO);
return devolucao.toISOString().slice(0, 10);
}
Ela recebe uma data e devolve uma data, e qualquer pessoa a lê de uma vez.
Em um processo, KISS pede o mesmo a cada passo: o menor número de documentos que guardam o que o projeto sabe, o menor número de comandos que levam uma entrega, o menor número de marcas que dizem onde ela está. Um passo que alguém recém-chegado não consegue explicar depois de ler uma vez é um passo a questionar.
YAGNI: você não vai precisar disso
YAGNI, "you aren't gonna need it" (você não vai precisar disso), vem do Extreme Programming, o método que Kent Beck descreveu em Extreme Programming Explained em 1999.2 Martin Fowler rastreia a frase até uma conversa no projeto onde o XP tomou forma: a cada capacidade que um colega dizia que o sistema logo precisaria, Beck respondia que eles não iam precisar dela.3 A regra: uma capacidade que você presume que o software vai precisar depois não é construída agora.
Uma feature presumida de que ninguém precisa desperdiça o esforço de construí-la, e uma que afinal é necessária costuma precisar ser refeita, porque as necessidades mudaram enquanto ela esperava. Até a feature presumida que se mostra certa paga o atraso e a carga: a feature que tem valor agora espera enquanto ela é construída, e o código dela "acrescenta alguma complexidade ao software" enquanto ficar.3 Suponha que alguém espere que a biblioteca empreste por prazos diferentes a tipos diferentes de membro, e escreva isso agora:
type CategoriaDeMembro = "adulto" | "crianca" | "pesquisador";
type PoliticaDeEmprestimo = {
diasPara(categoria: CategoriaDeMembro): number;
renovacoes(categoria: CategoriaDeMembro): number;
};
O documento de produto não nomeia nenhuma categoria e nenhuma renovação.
Todo leitor de emprestar agora precisa aprender uma política que a biblioteca não tem, todo teste precisa passar uma, e quando as categorias chegarem, não serão estas três.
DIAS_DE_EMPRESTIMO = 21 é a regra inteira até que uma entrega peça mais.
Fowler traça o limite no mesmo artigo: YAGNI "só se aplica a capacidades construídas no software para sustentar uma feature presumida, não se aplica ao esforço de deixar o software mais fácil de modificar".3 Um teste, um nome claro, uma regra tirada de uma tela para uma função própria: isso é qualidade interna, e YAGNI nunca argumenta contra ela. YAGNI só funciona em código fácil de mudar, porque a promessa é que você vai acrescentar a capacidade quando ela for necessária, e mudança barata é o que torna essa promessa verdadeira.
DRY: um lugar para cada conhecimento
Andy Hunt e Dave Thomas deram nome ao DRY, "don't repeat yourself" (não se repita), em The Pragmatic Programmer em 1999: "Todo conhecimento deve ter uma representação única, inequívoca e autoritativa dentro de um sistema."4 A unidade é o conhecimento. Duas linhas idênticas podem ser dois conhecimentos, e um conhecimento pode estar escrito em duas linhas que não se parecem em nada.
"Um empréstimo dura 21 dias" é um conhecimento.
Escrito como 21 em dataDeDevolucao e de novo como "Devolução em 21 dias" na tela de empréstimo, ele está em dois lugares, e no dia em que a biblioteca passar para 14 dias um deles vai ser esquecido.
A tela importa DIAS_DE_EMPRESTIMO, e o conhecimento mora em um lugar só.
"O nome de um membro não pode ser vazio" e "o título de um livro não pode ser vazio" são dois conhecimentos que por acaso se leem igual hoje.
Junte os dois em uma regra naoVazio compartilhada por membros e livros, e no dia em que um título puder ser vazio para um manuscrito sem título, a mudança chega aos membros também.
Sandi Metz deu nome à falha oposta em 2016: "duplicação é muito mais barata que a abstração errada".5 A abstração errada é a função compartilhada que dois chamadores usavam por razões diferentes; cada novo chamador acrescenta um parâmetro e uma condição, até que ninguém consiga mudá-la sem quebrar alguém. O remédio dela é devolver o código a cada chamador e recomeçar do que cada um precisa. DRY e Metz concordam: um lugar para cada conhecimento, e uma versão compartilhada só para o que é o mesmo conhecimento.
Abstração na segunda ocorrência
Os três princípios se encontram em uma regra, que o processo deste livro escreve em todo projeto: uma abstração é escrita na segunda ocorrência concreta, e a entrega que a escreve diz qual foi a primeira. Uma cópia não é prova de que uma versão compartilhada seja necessária: é um palpite, e YAGNI recusa palpites. Duas cópias são o erro que já aconteceu, e DRY pede um lugar só. Nomear a primeira cópia torna o movimento verificável: quem revisa abre as duas e vê que são o mesmo conhecimento, que é o teste de Metz.
Na biblioteca de empréstimos, Result, o tipo que carrega um valor ou o que o impediu (a seção dele vem a seguir), aparece primeiro dentro da feature de empréstimos, como o tipo de retorno de emprestar em features/emprestimos/regras.ts.
Ele fica lá enquanto tem um usuário.
Depois, uma entrega de membros precisa de uma regra que pode recusar, e Result seria escrito uma segunda vez; essa entrega o move para src/lib/result.ts, a pasta do código que não pertence a nenhuma feature, e diz isso:
// Primeiro uso: features/emprestimos/regras.ts (emprestar).
// Segundo uso: features/membros/regras.ts (cadastrar).
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
ok e err, as duas funções que constroem um Result, vão junto, e as duas features os importam de lá.
Se o movimento tivesse vindo no primeiro uso, teria sido um palpite que por acaso deu certo; no segundo, é um fato com duas testemunhas.
Uma função pura
Uma regra que mora dentro de uma tela, de um controller ou de uma chamada ao banco de dados não pode ser testada sem essa tela, esse controller ou esse banco. Uma função é pura quando a mesma entrada sempre dá a mesma saída e nada mais acontece: nenhuma leitura de banco de dados, nenhuma escrita em arquivo, nenhum relógio, nenhuma rede. O teste dela é dado entrando, valor saindo, e não precisa de preparação. Uma função impura precisa que o mundo seja arrumado antes de rodar, e todo teste paga por essa arrumação.
Na biblioteca de empréstimos, a regra "um membro com um livro atrasado não pode pegar outro" é uma função pura:
type Membro = { id: string; emAtraso: number };
// Um tipo literal de string: seu único valor é esta string.
// Onde se espera uma Recusa, como no err abaixo, o compilador
// aceita só esta string, e um erro de digitação não compila.
type Recusa = "TemLivrosEmAtraso";
function podePegarEmprestado(membro: Membro): Result<Membro, Recusa> {
if (membro.emAtraso > 0) return err("TemLivrosEmAtraso");
return ok(membro);
}
Ela lê um membro e responde.
De onde o membro veio, um banco de dados ou um teste, não é assunto dela.
Imutabilidade é a mesma ideia aplicada aos dados: uma função que muda a sua entrada esconde uma segunda saída, então podePegarEmprestado devolve o membro que recebeu ou uma recusa, e não muda nada.
A Clean Architecture, como costuma ser praticada, injeta os repositórios no caso de uso que guarda a regra, e o teste dele passa a precisar de um repositório falso para cada um; este livro mantém podePegarEmprestado pura, recebendo um membro e não um jeito de buscá-lo, e deixa buscar e salvar para um orquestrador, a função em volta da regra.
Result: o valor que carrega uma falha
Result é um tipo que guarda o valor ou o que o impediu:
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
Quem chama lê ok antes de alcançar value ou error, e o compilador estreita o tipo nessa checagem.
O motivo para carregar uma falha em um valor, seja qual for a forma, um union type como aqui ou um enum, é o que o compilador consegue fazer com ela: todo resultado que uma regra pode produzir está nomeado no tipo dela, então o código que os trata pode ser conferido contra essa lista.
O tipo torna a checagem possível e não a executa: um switch que deixa um caso de fora ainda compila quando tem um default ou não devolve nada, e quando falha, o erro dele não nomeia o caso, a menos que o código que trata a falha peça a checagem.
Quando o tratamento a pede, a checagem é o valor inteiro desta construção: o compilador, e não uma revisão de código, garante que nenhum caso foi esquecido, no dia em que a regra é escrita e no dia em que um caso é acrescentado.
A seção "Todo caso tratado" mostra duas maneiras de pedi-la em TypeScript.
A área chama o princípio de "errors as values", erros como valores, a partir do "erros são valores" do Go;6 este livro diz "exceções como valores", e a próxima seção diz por quê.
Exceção, recusa, erro
Uma falha é uma de três coisas, e na maioria das linguagens o tipo não diz qual.
O livro do Rust diz isso de um bug e de uma falha que quem chama trata: "a maioria das linguagens não distingue esses dois tipos de erro e trata os dois do mesmo jeito, usando mecanismos como exceções".8
As falhas nativas do JavaScript são todas um Error,9 e as do .NET são todas uma Exception,10 então quem decide é a causa.
Algumas linguagens separam, sim, o bug da falha que quem chama trata, cada uma com o seu mecanismo.
Rust devolve um Result para a falha que quem chama trata e interrompe com panic! em um bug: "erros irrecuperáveis são sempre sintomas de bugs".8
Swift tem throws para o que ele chama de "erros recuperáveis em tempo de execução", as exceções deste livro, enquanto uma asserção ou uma pré-condição que falha interrompe o programa, como fatalError faz: "não há como capturar uma asserção que falhou".11
Go devolve um valor error, "o jeito usual de reportar um erro", a exceção deste livro, e guarda panic para quando "algo impossível aconteceu", como o seu runtime faz ao "indexar um slice fora dos limites".12
Java também traça a linha, mas não pelo nome.
O seu Error marca "problemas sérios que uma aplicação razoável não deveria tentar capturar", e as suas exceções de runtime são "o resultado de um problema de programação", do qual não se pode "razoavelmente esperar" que quem chama se recupere; mesmo assim, NullPointerException estende RuntimeException, que estende Exception, então um catch de Exception captura também o bug.13
Dart é a mais explícita que eu conheço, porque põe a linha nos nomes dos seus dois tipos raiz: o seu dart:core tem uma classe Exception, "feita para ser capturada", e uma classe Error, para "uma falha do programa que o programador deveria ter evitado", com RangeError, StateError e TypeError entre os seus erros.
Uma falha que quem chama precisa capturar, diz a sua documentação, é "uma exceção, e não um erro".7
- Uma exceção é uma falha esperada vinda de fora do programa: o banco de dados caiu, a rede caiu, o disco encheu, um arquivo não está lá.
Nenhum código está errado.
Ela é capturada na fronteira com o mundo de fora e devolvida como um valor, por dois motivos.
A exceção pertence ao domínio de onde foi lançada: uma biblioteca de login lança a sua própria
AppleSignInException, e o domínio da aplicação não tem ideia do que é o login com Apple; tudo o que ele precisa saber é que o login falhou. Traduzida em um valor,LoginFalhou, a falha passa a falar a língua da aplicação e, como membro de um tipo, pode entrar na checagem exaustiva: um tratamento que pede a checagem precisa tratar toda exceção que a aplicação pode encontrar, e o compilador prova isso. - Uma recusa é uma regra dizendo não: um membro com livros atrasados, um telefone com dígitos de menos, um horário já ocupado. Nada falhou; a regra fez o seu trabalho. Ela também é devolvida como um valor, e é o resultado mais comum que um programa trata.
- Um erro é um bug: código que está errado. Ele nunca é capturado. Ele chega à sua tela enquanto você desenvolve e ao seu analytics quando o programa roda, para que você o corrija.
Por isso este livro não diz "erros como valores".
Um erro é um bug, e um bug nunca é um valor: é uma correção.
O que viaja em um Result é uma exceção ou uma recusa.
Por que não lançar
Uma função devolve um valor, e a sua assinatura diz qual.
Um throw usado para conduzir o fluxo, no lugar de um return, custa três coisas.
Ele sai por uma porta que o ponto de chamada não mostra: a função "devolve" algo que a assinatura nunca mencionou, por um caminho que quem chama não escreveu, e cada chamada dentro dela é mais uma porta dessas.
Ele faz a assinatura mentir: function emprestar(...): Emprestimo não diz nada sobre as três maneiras de falhar, então o compilador não consegue verificar que todo caso é tratado, enquanto Result<Emprestimo, RecusaDeEmprestimo> diz isso no tipo, e um tratamento que precisa nomear cada recusa não compila quando falta uma.
E o catch largo o bastante para conduzir o fluxo também captura os bugs que acontecem dentro dele, e eles somem em uma mensagem que ninguém lê.
A maior parte do que um programa trata é uma regra dizendo não, então um programa que lança para recusas tem uma saída escondida em cada regra, quem chama e o agente que lê o código precisam saber de cor quais falhas eram intencionais, e cada recusa paga por uma exceção lançada e capturada: no benchmark de Stephen Toub, mil throws, cada um capturado através de dez chamadas assíncronas, levaram cerca de um oitavo de segundo no .NET 8.14
Onde mora o catch
Exceções só existem onde o programa toca o mundo de fora: uma chamada ao banco de dados, uma requisição de rede, um arquivo, o armazenamento do celular.
Então esse é o único lugar onde mora um try/catch, e ele faz uma coisa: transforma a exceção da biblioteca no valor do programa, e deixa passar qualquer outro throw.
O catch precisa reconhecer a exceção da biblioteca, e a biblioteca diz como. O driver de banco de dados da biblioteca de empréstimos, no exemplo deste capítulo, lança uma classe própria:
class FalhaDoDriver extends Error {}
Um driver real documenta o próprio sinal, uma classe ou um código na falha, e a fronteira testa esse sinal; instanceof Error ou o texto da mensagem também casariam com um bug.
type BancoDeDadosFalhou = { codigo: "BancoDeDadosFalhou"; mensagem: string };
function consultar<T>(executar: () => T): Result<T, BancoDeDadosFalhou> {
try {
return ok(executar());
} catch (lancado) {
if (!(lancado instanceof FalhaDoDriver)) throw lancado;
return err({ codigo: "BancoDeDadosFalhou", mensagem: lancado.message });
}
}
Todo o resto sai de consultar como entrou, o mesmo objeto: um TypeError de um bug dentro de executar, qualquer outro Error, uma string lançada, o fluxo de controle próprio de um framework, lançado de propósito.
Um catch que pegasse tudo isso transformaria o bug em BancoDeDadosFalhou, e o programa diria ao usuário que o banco de dados falhou enquanto o que está errado é o código.
consultar trata uma chamada que tem a resposta quando retorna.
Uma chamada ao banco de dados costuma ser assíncrona: ela devolve na hora uma promise, o valor que o JavaScript dá para um resultado ainda a caminho, e a falha vem depois, como uma rejeição dessa promise.
Passada a consultar, a promise é apenas o que executar devolveu, então consultar devolve ok com ela dentro: um catch vê um throw, e uma rejeição só vira um quando alguém espera a promise com await, por isso a rejeição escapa do Result.
Uma chamada assíncrona passa por uma função própria, que espera a promise dentro do try:
async function consultarAsync<T>(
executar: () => Promise<T>,
): Promise<Result<T, BancoDeDadosFalhou>> {
try {
return ok(await executar());
} catch (lancado) {
if (!(lancado instanceof FalhaDoDriver)) throw lancado;
return err({ codigo: "BancoDeDadosFalhou", mensagem: lancado.message });
}
}
O await lança uma rejeição onde espera,16 aqui dentro do try, então a falha do driver vira err, e qualquer outra rejeição rejeita a promise de consultarAsync com o mesmo motivo.
Toda chamada ao banco de dados roda dentro de consultar ou consultarAsync, e nada depois delas sabe qual biblioteca de banco de dados está em uso.
O domain-driven design chama um tradutor assim de camada anticorrupção, cujo "propósito central ... é proteger o modelo de domínio";15 este livro dá um passo a mais e traduz as exceções também.
Troque a biblioteca de banco de dados e a fronteira muda, essas duas funções e o sinal que elas testam; o resto do programa não.
A mesma fronteira pode transformar uma exceção em uma recusa.
Quando dois membros pegam o último exemplar no mesmo instante, um índice único no banco de dados recusa a segunda escrita.
O código que rodou a escrita lê essa falha, o aviso do driver de que esse índice único a recusou, e devolve a recusa JaEmprestado, já que nada quebrou: a regra valeu, no único lugar onde duas requisições não podem competir.
Qualquer outra falha da mesma escrita, outra restrição ou uma conexão perdida, continua BancoDeDadosFalhou.
Então a regra fica assim: capture na fronteira com o mundo de fora, reconheça a exceção da biblioteca, traduza, e deixe todo o resto passar.
Todo caso tratado
Um Result compensa quando o compilador confere o tratamento, e o tratamento precisa pedir a checagem.
Em TypeScript, um Record sobre o tipo da recusa precisa nomear cada membro:
type RecusaDeEmprestimo = "TemLivrosEmAtraso" | "JaEmprestado" | "MembroSuspenso";
const mensagem: Record<RecusaDeEmprestimo, string> = {
TemLivrosEmAtraso: "Devolva primeiro os seus livros atrasados.",
JaEmprestado: "Este exemplar acabou de ser emprestado a outra pessoa.",
MembroSuspenso: "A sua matrícula está suspensa.",
};
Deixe MembroSuspenso de fora de mensagem, e o build falha com um erro que nomeia a propriedade que falta.
Acrescente uma recusa às regras e esqueça a mensagem dela, e ele falha do mesmo jeito, antes que algum usuário veja uma tela em branco.
Um Record serve quando cada caso dá um valor.
Quando um caso precisa executar código, um switch pede a mesma checagem com never, o tipo que não tem valores.
Depois de cada case, o compilador estreita a recusa aos casos ainda não atendidos, então no ramo default, quando todo caso está tratado, só resta never.
Aqui o switch dá as mesmas mensagens que o Record, para que os dois possam ser comparados:
function mensagemPara(recusa: RecusaDeEmprestimo): string {
switch (recusa) {
case "TemLivrosEmAtraso":
return "Devolva primeiro os seus livros atrasados.";
case "JaEmprestado":
return "Este exemplar acabou de ser emprestado a outra pessoa.";
case "MembroSuspenso":
return "A sua matrícula está suspensa.";
default: {
const naoTratada: never = recusa;
return naoTratada;
}
}
}
Deixe o caso MembroSuspenso de fora, e essa recusa chega ao default; ela não é never, então a linha que declara naoTratada não compila, com um erro que nomeia "MembroSuspenso".
Acrescente uma recusa ao union, e tanto mensagem quanto mensagemPara falham até que cada um a trate.17
Sem essa linha, um switch cujo default devolve uma mensagem genérica, como "Algo deu errado.", compila com um caso de fora, e também um switch cujos ramos não devolvem nada; o membro suspenso então lê a mensagem genérica, ou nada, onde a regra tinha uma precisa.
Um default não prova que os casos estão completos; ele esconde o que falta.
Em Rust e em Dart, um match ou um switch exaustivo dá a mesma checagem; em Go, um linter.
Os mesmos princípios em um processo
Cuidado. Um agente escreve mais do que código nessa velocidade: escreve também scripts, verificações, regras e passos, cada um plausível, e um processo cresce como o código cresce, uma adição razoável por vez. Enquanto este livro era escrito, o agente propôs que todo marco terminasse com um passo de revisão, uma boa ideia; a revisão achou erros, que abriram um marco de correções, cuja própria revisão abriu um segundo, e um terceiro estava a caminho quando eu o interrompi. O que eu devia estar fazendo era ler os capítulos e dizer o que mudar. Por conta própria, o agente tinha entrado numa espiral de verificações que verificavam verificações, e uma pergunta é o que a interrompe: que erro concreto este passo teria pegado?
Este livro chama essa pergunta de regulador e faz dela a regra que todo passo do processo precisa responder: um passo entra quando nomeia o erro concreto que teria pegado, e sai quando não nomeia nenhum.
O que o time ganha
Menos para ler, menos para revisar e menos para manter verdadeiro: cada arquivo, opção e verificação que não é escrito é um que nenhuma pessoa revisa, nenhum agente carrega em uma sessão e nenhuma entrega precisa satisfazer, enquanto o Ninjobs fazia toda entrega passar por 29 verificações que nunca tinham pegado um erro.
Toda regra é uma função que um teste chama com dados, então uma regra tem um teste antes de ter uma tela, e um agente a quem se pede para mudá-la a encontra em um lugar só, com os casos dela nomeados no tipo.
Toda falha que quem chama precisa tratar está na assinatura, então quem revisa lê o tipo e sabe o que pode dar errado, e um tratamento escrito como um Record ou com uma checagem never transforma um caso esquecido em um erro de compilação.
Pontos-chave
- A complexidade acidental é a parte que as pessoas acrescentam; ela é uma escolha, e KISS (a forma mais simples que faz o trabalho), YAGNI (nada construído para uma necessidade presumida) e DRY (um lugar para cada conhecimento) são três maneiras de recusá-la.
- Uma abstração é escrita na segunda ocorrência concreta, e a entrega diz qual foi a primeira; em um processo, um passo só entra quando nomeia o erro concreto que teria pegado.
- Uma regra de negócio é uma função pura: dados entrando, um
Resultsaindo, sem I/O e sem relógio; buscar e salvar pertencem ao orquestrador em volta dela. - Uma falha é uma exceção (de fora, reconhecida e capturada na fronteira, aguardada dentro do
tryquando a chamada é assíncrona, e devolvida nas palavras do próprio domínio), uma recusa (uma regra dizendo não, devolvida como valor) ou um erro (um bug, nunca capturado); um throw não conduz fluxo. - O valor de um
Resulté a checagem exaustiva, e o tratamento a pede: umRecord, ou umswitchcujodefaultprecisa sernever, não compila quando falta um caso; umswitchcom umdefaultgenérico compila.
-
Frederick P. Brooks Jr., "No Silver Bullet: Essence and Accidents of Software Engineering", IEEE Computer 20(4), 1987. ↩
-
Kent Beck, "Extreme Programming Explained: Embrace Change", Addison-Wesley, 1999. ↩
-
Martin Fowler, "Yagni", martinfowler.com, 2015-05-26, acesso em 2026-09-30. https://martinfowler.com/bliki/Yagni.html ↩↩↩
-
Andrew Hunt e David Thomas, "The Pragmatic Programmer", Addison-Wesley, 1999; edição de 20 anos, 2019. ↩
-
Sandi Metz, "The Wrong Abstraction", 2016-01-20, acesso em 2026-09-30. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction ↩
-
Rob Pike, "Errors are values", The Go Blog, 2015. https://go.dev/blog/errors-are-values ↩
-
Dart, "Exception class" e "Error class", referência da API
dart:core, acesso em 2026-09-29. https://api.dart.dev/stable/dart-core/Exception-class.html e https://api.dart.dev/stable/dart-core/Error-class.html ↩ -
The Rust Project, "Error Handling" e "To
panic!or Not topanic!", The Rust Programming Language, capítulo 9, acesso em 2026-10-18. https://doc.rust-lang.org/book/ch09-00-error-handling.html e https://doc.rust-lang.org/book/ch09-03-to-panic-or-not-to-panic.html ↩↩ -
MDN Web Docs, "Error", referência de JavaScript, acesso em 2026-10-18. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error ↩
-
Microsoft, "Exception Class", referência da API do .NET, Microsoft Learn, acesso em 2026-10-18. https://learn.microsoft.com/en-us/dotnet/api/system.exception ↩
-
Swift.org, "The Basics" e "Error Handling", The Swift Programming Language, acesso em 2026-10-18. https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/#Assertions-and-Preconditions e https://docs.swift.org/swift-book/documentation/the-swift-programming-language/errorhandling/ ↩
-
The Go Authors, "Effective Go", seções "Errors" e "Panic", go.dev, acesso em 2026-10-18. https://go.dev/doc/effective_go ↩
-
Oracle, "Class Error" e "Class NullPointerException", API do Java SE 21, e "Unchecked Exceptions: The Controversy", The Java Tutorials, acesso em 2026-10-18. https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Error.html, https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/NullPointerException.html e https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html ↩
-
Stephen Toub, "Performance Improvements in .NET 9", .NET Blog, 2024-09-12, seção "VM", benchmark
ExceptionThrowCatch, acesso em 2026-09-30. https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/ ↩ -
Microsoft, "Anti-Corruption Layer pattern", Azure Architecture Center, acesso em 2026-09-30. https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer ↩
-
Ecma International, "ECMAScript Language Specification", seções "Await" e "The try Statement", acesso em 2026-10-18. https://tc39.es/ecma262/multipage/control-abstraction-objects.html#await e https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-try-statement ↩
-
Microsoft, "Narrowing", seção "Exhaustiveness checking", e "Utility Types", seção "Record
", TypeScript Handbook, acesso em 2026-10-18. https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking e https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type ↩