Bugs e Erros

Depurar é duas vezes mais difícil do que escrever o código em primeiro lugar. Portanto, se você escreve o código da forma mais esperta possível, você, por definição, não é inteligente o suficiente para depurá-lo.

Brian Kernighan and P.J. Plauger, The Elements of Programming Style
Ilustração mostrando vários insetos e uma centopeia

Falhas em programas de computador são geralmente chamadas de bugs. Isso faz os programadores se sentirem bem ao imaginá-las como pequenas coisas que simplesmente entram rastejando no nosso trabalho. Na realidade, é claro, nós mesmos as colocamos lá.

Se um programa é pensamento “cristalizado”, podemos categorizar aproximadamente os bugs entre aqueles causados por pensamentos confusos e aqueles causados por erros introduzidos ao converter um pensamento em código. O primeiro tipo geralmente é mais difícil de diagnosticar e corrigir do que o segundo.

Linguagem

Muitos erros poderiam ser apontados automaticamente pelo computador se ele soubesse o suficiente sobre o que estamos tentando fazer. Mas aqui, a flexibilidade do JavaScript é um obstáculo. Seu conceito de ligações e propriedades é vago o bastante para que ele raramente detecte erro de digitaçãos antes de realmente executar o programa. Mesmo assim, ele permite que você faça algumas coisas claramente sem sentido sem reclamar, como calcular true * "monkey".

Há algumas coisas das quais o JavaScript reclama. Escrever um programa que não segue a gramática da linguagem fará o computador reclamar imediatamente. Outras coisas, como chamar algo que não é uma função ou acessar uma propriedade em um valor undefined, causarão um erro quando o programa tentar realizar a ação.

Muitas vezes, porém, seu cálculo sem sentido apenas produzirá NaN (not a number) ou um valor undefined, enquanto o programa continua feliz, convencido de que está fazendo algo significativo. O erro só se manifestará mais tarde, depois que o valor incorreto tiver passado por várias funções. Pode nem gerar um erro, mas causar silenciosamente uma saída incorreta do programa. Encontrar a origem desses problemas pode ser difícil.

O processo de encontrar erros—bugs—em programas é chamado de debugging.

Modo estrito

O JavaScript pode se tornar um pouco mais rigoroso ao habilitar o strict mode. Isso pode ser feito colocando a string "use strict" no topo de um arquivo ou do corpo de uma função. Veja um exemplo:

function canYouSpotTheProblem() {
  "use strict";
  for (counter = 0; counter < 10; counter++) {
    console.log("Happy happy");
  }
}

canYouSpotTheProblem();
// → ReferenceError: counter is not defined

Código dentro de classes e módulos (que discutiremos no Capítulo 10) é automaticamente estrito. O comportamento antigo não estrito ainda existe apenas porque códigos antigos podem depender dele, e os designers da linguagem trabalham duro para evitar quebrar programas existentes.

Normalmente, quando você esquece de colocar let antes de sua ligação, como com counter no exemplo, o JavaScript cria silenciosamente uma ligação global e a usa. No modo estrito, um erro é reportado. Isso é muito útil. Vale notar, porém, que isso não funciona quando a ligação já existe em algum escopo. Nesse caso, o loop ainda sobrescreverá silenciosamente o valor da ligação.

Outra mudança no modo estrito é que a ligação this contém o valor undefined em funções que não são chamadas como métodos. Fora do modo estrito, this se refere ao objeto de escopo global, cujas propriedades são as ligações globais. Portanto, se você chamar um método ou construtor incorretamente no modo estrito, o JavaScript produzirá um erro assim que tentar ler algo de this, em vez de escrever no escopo global.

Por exemplo, considere o código a seguir, que chama uma função construtora sem a palavra-chave new, de modo que seu this não se refere a um objeto recém-criado:

function Person(name) { this.name = name; }
let ferdinand = Person("Ferdinand"); // oops
console.log(name);
// → Ferdinand

A chamada incorreta a Person funcionou, mas retornou um valor undefined e criou a ligação global name. No modo estrito, o resultado é diferente.

"use strict";
function Person(name) { this.name = name; }
let ferdinand = Person("Ferdinand"); // forgot new
// → TypeError: Cannot set property 'name' of undefined

Somos informados imediatamente de que algo está errado. Isso é útil.

Felizmente, construtores criados com a notação class sempre reclamarão se forem chamados sem new, tornando isso um problema menor mesmo fora do modo estrito.

O modo estrito faz mais algumas coisas. Ele proíbe funções com múltiplos parâmetros com o mesmo nome e remove completamente certos recursos problemáticos da linguagem (como a instrução with, que é tão ruim que não será discutida neste livro).

Em resumo, colocar "use strict" no topo do seu programa raramente prejudica e pode ajudar a detectar problemas.

Tipos

Algumas linguagens querem saber os tipos de todos as suas ligações e expressões antes mesmo de executar um programa. Elas informarão imediatamente quando um tipo for usado de maneira inconsistente. O JavaScript considera tipos apenas durante a execução do programa e, mesmo assim, frequentemente tenta converter valores implicitamente para o tipo esperado, então não ajuda muito.

Ainda assim, tipos fornecem uma estrutura útil para falar sobre programas. Muitos erros vêm de confusão sobre o tipo de valor que entra ou sai de uma função. Se você tiver essa informação anotada, é menos provável que se confunda.

Você poderia adicionar um comentário como o seguinte antes da função findRoute do capítulo anterior para descrever seu tipo:

// (graph: Object, from: string, to: string) => string[]
function findRoute(graph, from, to) {
  // ...
}

Existem várias convenções diferentes para anotar programas JavaScript com tipos.

Uma coisa sobre tipos é que eles precisam introduzir sua própria complexidade para conseguir descrever código suficiente e serem úteis. Qual você acha que seria o tipo da função randomPick, que retorna um elemento aleatório de um array? Você precisaria introduzir uma variável de tipo, T, que pode representar qualquer tipo, para então dar a randomPick um tipo como (T[]) → T (função de um array de Ts para um T).

Quando os tipos de um programa são conhecidos, é possível que o computador os verifique para você, apontando erros antes da execução. Existem vários dialetos de JavaScript que adicionam tipos à linguagem e os verificam. O mais popular se chama TypeScript. Se você tiver interesse em adicionar mais rigor aos seus programas, recomendo experimentar.

Neste livro, continuaremos usando código JavaScript puro, perigoso e sem tipagem.

Testes

Se a linguagem não vai nos ajudar muito a encontrar erros, teremos que encontrá-los da maneira difícil: executando o programa e verificando se ele faz a coisa certa.

Fazer isso manualmente, repetidas vezes, é uma péssima ideia. Além de ser irritante, tende a ser ineficaz, já que leva muito tempo testar tudo exaustivamente a cada mudança.

Computadores são bons em tarefas repetitivas, e testes são o exemplo ideal disso. Testes automatizados são o processo de escrever um programa que testa outro programa. Escrever testes dá um pouco mais de trabalho do que testar manualmente, mas, depois de feito, você ganha uma espécie de superpoder: leva apenas alguns segundos para verificar se seu programa ainda se comporta corretamente em todas as situações para as quais você escreveu testes. Quando algo quebra, você percebe imediatamente, em vez de descobrir por acaso mais tarde.

Testes geralmente assumem a forma de pequenos programas rotulados que verificam algum aspecto do seu código. Por exemplo, um conjunto de testes para o método (padrão, provavelmente já testado por alguém) toUpperCase poderia ser assim:

function test(label, body) {
  if (!body()) console.log(`Failed: ${label}`);
}

test("convert Latin text to uppercase", () => {
  return "hello".toUpperCase() == "HELLO";
});
test("convert Greek text to uppercase", () => {
  return "Χαίρετε".toUpperCase() == "ΧΑΊΡΕΤΕ";
});
test("don't convert case-less characters", () => {
  return "مرحبا".toUpperCase() == "مرحبا";
});

Escrever testes assim tende a produzir código repetitivo e meio desajeitado. Felizmente, existem ferramentas que ajudam a construir e executar coleções de testes (suítes de teste), fornecendo uma linguagem (na forma de funções e métodos) adequada para expressar testes e exibindo informações úteis quando um teste falha. Essas ferramentas são geralmente chamadas de test runners.

Alguns códigos são mais fáceis de testar do que outros. Em geral, quanto mais objetos externos o código manipula, mais difícil é configurar o contexto para testá-lo. O estilo de programação mostrado no capítulo anterior, que usa valores persistentes e autocontidos em vez de modificar objetos, tende a ser fácil de testar.

Depuração

Assim que você percebe que há algo errado com seu programa—porque ele se comporta mal ou produz erros—o próximo passo é descobrir qual é o problema.

Às vezes é óbvio. A mensagem de erro apontará para uma linha específica do programa, e ao olhar a descrição do erro e essa linha, muitas vezes você consegue ver o problema.

Mas nem sempre. Às vezes, a linha que disparou o problema é apenas o primeiro lugar onde um valor defeituoso produzido em outro ponto é usado de maneira inválida. Se você fez os exercícios dos capítulos anteriores, provavelmente já passou por situações assim.

O programa a seguir tenta converter um número inteiro em uma string em uma base dada (decimal, binária etc.) pegando repetidamente o último dígito e depois dividindo o número para removê-lo. Mas a saída estranha que ele produz sugere que há um bug.

function numberToString(n, base = 10) {
  let result = "", sign = "";
  if (n < 0) {
    sign = "-";
    n = -n;
  }
  do {
    result = String(n % base) + result;
    n /= base;
  } while (n > 0);
  return sign + result;
}
console.log(numberToString(13, 10));
// → 1.5e-3231.3e-3221.3e-3211.3e-3201.3e-3191.3e-3181.3…

Mesmo que você já veja o problema, finja por um momento que não. Sabemos que o programa está com defeito e queremos descobrir por quê.

Aqui você deve resistir à vontade de fazer mudanças aleatórias no código para ver se melhora. Em vez disso, pense. Analise o que está acontecendo e formule uma teoria sobre por que isso pode estar ocorrendo. Depois, faça observações adicionais para testar essa teoria—ou, se ainda não tiver uma, faça observações para ajudar a formulá-la.

Inserir algumas chamadas estratégicas de console.log no programa é uma boa forma de obter mais informações sobre o que ele está fazendo. Neste caso, queremos que n assuma os valores 13, 1 e depois 0. Vamos imprimir seu valor no início do loop.

13
1.3
0.13
0.013
…
1.5e-323

Certo. Dividir 13 por 10 não produz um número inteiro. Em vez de n /= base, o que queremos é n = Math.floor(n / base) para que o número seja corretamente “deslocado” para a direita.

Uma alternativa ao uso de console.log para inspecionar o comportamento do programa é usar os recursos de debugger do navegador. Navegadores permitem definir um breakpoint em uma linha específica do código. Quando a execução atinge essa linha, ela é pausada, e você pode inspecionar os valores das ligações naquele ponto. Não entrarei em detalhes, pois os debuggers variam entre navegadores, mas procure nas ferramentas de desenvolvedor do seu navegador ou busque instruções na web.

Outra forma de definir um breakpoint é incluir uma instrução debugger (composta apenas por essa palavra-chave) no programa. Se as ferramentas de desenvolvedor estiverem ativas, o programa pausará ao atingir essa instrução.

Propagação de erros

Nem todos os problemas podem ser prevenidos pelo programador. Se seu programa se comunica com o mundo externo de alguma forma, é possível receber entradas malformadas, ficar sobrecarregado ou ter falhas de rede.

Se você programa apenas para si mesmo, pode ignorar esses problemas até que ocorram. Mas se está construindo algo para outras pessoas usarem, geralmente é melhor que o programa faça algo melhor do que simplesmente travar. Às vezes, o certo é lidar com a entrada ruim e continuar executando. Em outros casos, é melhor informar ao usuário o que deu errado e então encerrar. Em qualquer situação, o programa precisa reagir ativamente ao problema.

Suponha que você tenha uma função promptNumber que pede um número ao usuário e o retorna. O que ela deve retornar se o usuário digitar “orange”?

Uma opção é retornar um valor especial. Escolhas comuns são null, undefined ou -1.

function promptNumber(question) {
  let result = Number(prompt(question));
  if (Number.isNaN(result)) return null;
  else return result;
}

console.log(promptNumber("How many trees do you see?"));

Agora, qualquer código que chame promptNumber deve verificar se um número real foi lido e, caso contrário, tentar se recuperar—talvez perguntando novamente ou usando um valor padrão. Ou pode retornar outro valor especial para seu próprio chamador, indicando falha.

Em muitas situações, principalmente quando erros são comuns e o chamador deve tratá-los explicitamente, retornar um valor especial é uma boa forma de indicar erro. Mas isso tem desvantagens. Primeiro, e se a função já puder retornar todos os tipos possíveis de valores? Nesse caso, você terá que encapsular o resultado em um objeto para distinguir sucesso de falha, como o método next dos iteradores faz.

function lastElement(array) {
  if (array.length == 0) {
    return {failed: true};
  } else {
    return {value: array[array.length - 1]};
  }
}

O segundo problema é que isso pode levar a código desajeitado. Se um trecho chama promptNumber 10 vezes, precisa verificar 10 vezes se retornou null. Se a resposta for apenas retornar null novamente, os chamadores dessa função também terão que verificar, e assim por diante.

Exceções

Quando uma função não pode continuar normalmente, muitas vezes gostaríamos de simplesmente parar tudo e saltar imediatamente para um ponto que saiba lidar com o problema. É isso que o tratamento de exceções faz.

Exceções são um mecanismo que permite que um código que encontra um problema lance (ou throw) uma exceção. Uma exceção pode ser qualquer valor. Lançá-la se parece com um retorno superpoderoso de uma função: ela sai não apenas da função atual, mas também de todas as chamadas até chegar à chamada inicial da execução. Isso é chamado de desenrolar da pilha. Você deve lembrar da pilha de chamadas do Capítulo 3. Uma exceção percorre essa pilha, descartando todos os contextos de chamada.

Se exceções sempre fossem até o fim da pilha, não seriam muito úteis. Elas apenas fariam seu programa explodir de forma diferente. O poder delas está no fato de que você pode colocar “obstáculos” na pilha para capturá-las com catch. Depois de capturar, você pode tratar o problema e continuar a execução.

Exemplo:

function promptDirection(question) {
  let result = prompt(question);
  if (result.toLowerCase() == "left") return "L";
  if (result.toLowerCase() == "right") return "R";
  throw new Error("Invalid direction: " + result);
}

function look() {
  if (promptDirection("Which way?") == "L") {
    return "a house";
  } else {
    return "two angry bears";
  }
}

try {
  console.log("You see", look());
} catch (error) {
  console.log("Something went wrong: " + error);
}

A palavra-chave throw lança uma exceção. Capturá-la é feito com um bloco try, seguido de catch. Quando o código em try lança uma exceção, o bloco catch é executado, com o nome entre parênteses ligado ao valor da exceção. Depois disso—ou se não houver erro—o programa continua após o try/catch.

Neste caso, usamos o construtor Error para criar a exceção. Ele cria um objeto com propriedade message. Instâncias de Error também armazenam informações sobre a pilha de chamadas no momento da exceção—um stack trace. Isso fica na propriedade stack e ajuda na depuração.

Note que look ignora totalmente a possibilidade de erro em promptDirection. Essa é a vantagem: o tratamento só precisa existir onde o erro ocorre e onde é tratado.

Bem, quase...

Limpando após exceções

O efeito de uma exceção é outro tipo de fluxo de controle. Toda ação que possa causar uma exceção — o que inclui praticamente toda chamada de função e acesso a propriedade — pode fazer com que o controle saia subitamente do seu código.

Isso significa que, quando um código possui vários efeitos colaterais, mesmo que seu fluxo de controle “normal” sugira que todos eles sempre ocorrerão, uma exceção pode impedir que alguns aconteçam.

Aqui está um código bancário realmente ruim:

const accounts = {
  a: 100,
  b: 0,
  c: 20
};

function getAccount() {
  let accountName = prompt("Enter an account name");
  if (!Object.hasOwn(accounts, accountName)) {
    throw new Error(`No such account: ${accountName}`);
  }
  return accountName;
}

function transfer(from, amount) {
  if (accounts[from] < amount) return;
  accounts[from] -= amount;
  accounts[getAccount()] += amount;
}

A função transfer transfere uma quantia de dinheiro de uma conta para outra, pedindo o nome da outra conta no processo. Se for fornecido um nome de conta inválido, getAccount lança uma exceção.

Mas transfer primeiro remove o dinheiro da conta e depois chama getAccount antes de adicioná-lo a outra conta. Se for interrompida por uma exceção nesse ponto, o dinheiro simplesmente desaparecerá.

Esse código poderia ter sido escrito de forma um pouco mais inteligente, por exemplo, chamando getAccount antes de começar a mover o dinheiro. Mas frequentemente problemas como esse ocorrem de maneiras mais sutis. Até mesmo funções que não parecem lançar exceções podem fazê-lo em circunstâncias excepcionais ou quando contêm um erro de programação.

Uma forma de lidar com isso é usar menos efeitos colaterais. Novamente, um estilo de programação que calcula novos valores em vez de modificar dados existentes ajuda. Se um trecho de código para no meio da criação de um novo valor, nenhuma estrutura de dados existente foi danificada, facilitando a recuperação.

Como isso nem sempre é prático, instruções try possuem outro recurso: elas podem ser seguidas por um bloco finally, seja em vez de ou além de um bloco catch. Um bloco finally diz: “não importa o que aconteça, execute este código após tentar executar o código no bloco try.”

function transfer(from, amount) {
  if (accounts[from] < amount) return;
  let progress = 0;
  try {
    accounts[from] -= amount;
    progress = 1;
    accounts[getAccount()] += amount;
    progress = 2;
  } finally {
    if (progress == 1) {
      accounts[from] += amount;
    }
  }
}

Esta versão da função acompanha seu progresso e, ao sair, se perceber que foi interrompida em um ponto onde deixou o programa em um estado inconsistente, repara o dano causado.

Note que, embora o código em finally seja executado quando uma exceção é lançada no bloco try, ele não interfere na exceção. Após a execução do bloco finally, a pilha continua sendo desempilhada.

Escrever programas que funcionem de forma confiável mesmo quando exceções surgem em lugares inesperados é difícil. Muitas pessoas simplesmente não se preocupam com isso e, como exceções normalmente são reservadas para situações excepcionais, o problema pode ocorrer tão raramente que nunca é percebido. Se isso é bom ou ruim depende de quanto dano o software causa quando falha.

Captura seletiva

Quando uma exceção chega até o fim da pilha sem ser capturada, ela é tratada pelo ambiente. O que isso significa varia entre ambientes. Em navegadores, uma descrição do erro geralmente é exibida no console do JavaScript (acessível pelo menu de ferramentas ou desenvolvedor do navegador). O Node.js, o ambiente JavaScript sem navegador que veremos no Capítulo 20, é mais cuidadoso com corrupção de dados. Ele encerra todo o processo quando ocorre uma exceção não tratada.

Para erros de programação, simplesmente deixar o erro acontecer geralmente é o melhor que você pode fazer. Uma exceção não tratada é uma forma razoável de sinalizar um programa quebrado, e o console JavaScript, em navegadores modernos, fornecerá informações sobre quais chamadas de função estavam na pilha quando o problema ocorreu.

Para problemas que são esperados durante o uso normal, encerrar com uma exceção não tratada é uma péssima estratégia.

Usos inválidos da linguagem, como referenciar uma ligação inexistente, acessar uma propriedade em null ou chamar algo que não é uma função, também resultarão em exceções. Essas exceções também podem ser capturadas.

Quando um bloco catch é executado, tudo que sabemos é que algo dentro do bloco try causou uma exceção. Mas não sabemos o que foi ou qual exceção foi gerada.

JavaScript (em uma omissão bastante evidente) não fornece suporte direto para captura seletiva de exceções: ou você captura todas ou não captura nenhuma. Isso torna tentador assumir que a exceção recebida é exatamente aquela que você tinha em mente ao escrever o bloco catch.

Mas pode não ser. Alguma outra suposição pode ter sido violada, ou você pode ter introduzido um bug que está causando uma exceção. Aqui está um exemplo que tenta continuar chamando promptDirection até obter uma resposta válida:

for (;;) {
  try {
    let dir = promtDirection("Where?"); // ← typo!
    console.log("You chose ", dir);
    break;
  } catch (e) {
    console.log("Not a valid direction. Try again.");
  }
}

A construção for (;;) é uma forma de criar intencionalmente um loop que não termina por conta própria. Saímos do loop apenas quando uma direção válida é fornecida. Infelizmente, escrevemos promptDirection errado, o que resultará em um erro de “variável indefinida”. Como o bloco catch ignora completamente o valor da exceção (e), assumindo que sabe qual é o problema, ele trata incorretamente o erro de ligação como se fosse entrada inválida. Isso não só causa um loop infinito, como também “esconde” a mensagem de erro útil sobre o nome incorreto.

Como regra geral, não capture exceções de forma genérica, a menos que seja para “encaminhá-las” para outro lugar — por exemplo, pela rede para avisar outro sistema que seu programa falhou. E mesmo assim, pense cuidadosamente sobre como você pode estar escondendo informações.

Queremos capturar um tipo específico de exceção. Podemos fazer isso verificando, no bloco catch, se a exceção recebida é aquela que nos interessa e, caso contrário, relançá-la. Mas como reconhecemos uma exceção?

Poderíamos comparar a propriedade message com a mensagem de erro esperada. Mas essa é uma forma frágil de escrever código — estaríamos usando uma informação destinada a humanos (a mensagem) para tomar uma decisão programática. Assim que alguém alterar (ou traduzir) a mensagem, o código deixará de funcionar.

Em vez disso, vamos definir um novo tipo de erro e usar instanceof para identificá-lo.

class InputError extends Error {}

function promptDirection(question) {
  let result = prompt(question);
  if (result.toLowerCase() == "left") return "L";
  if (result.toLowerCase() == "right") return "R";
  throw new InputError("Invalid direction: " + result);
}

A nova classe de erro estende Error. Ela não define seu próprio construtor, o que significa que herda o construtor de Error, que espera uma string como argumento. Na verdade, ela não define nada — a classe é vazia. Objetos InputError se comportam como objetos Error, exceto pelo fato de terem uma classe diferente, pela qual podemos identificá-los.

Agora o loop pode capturar essas exceções com mais cuidado.

for (;;) {
  try {
    let dir = promptDirection("Where?");
    console.log("You chose ", dir);
    break;
  } catch (e) {
    if (e instanceof InputError) {
      console.log("Not a valid direction. Try again.");
    } else {
      throw e;
    }
  }
}

Isso capturará apenas instâncias de InputError e deixará outras exceções passarem. Se você reintroduzir o erro de digitação, o erro de ligação indefinida será corretamente exibido.

Asserções

Asserções são verificações dentro de um programa que confirmam que algo está da forma como deveria estar. Elas são usadas não para tratar situações que podem ocorrer no uso normal, mas para encontrar erros de programação.

Se, por exemplo, firstElement é descrita como uma função que nunca deve ser chamada com arrays vazios, podemos escrevê-la assim:

function firstElement(array) {
  if (array.length == 0) {
    throw new Error("firstElement called with []");
  }
  return array[0];
}

Agora, em vez de retornar silenciosamente undefined (o que acontece ao acessar uma propriedade inexistente de um array), o programa falhará imediatamente ao ser usado incorretamente. Isso torna menos provável que esses erros passem despercebidos e mais fácil encontrar sua causa quando ocorrerem.

Não recomendo tentar escrever asserções para todo tipo possível de entrada inválida. Isso daria muito trabalho e deixaria o código muito poluído. O ideal é reservá-las para erros fáceis de cometer (ou que você percebe que comete com frequência).

Resumo

Uma parte importante da programação é encontrar, diagnosticar e corrigir bugs. Problemas podem se tornar mais fáceis de perceber se você tiver uma suíte de testes automatizados ou adicionar asserções aos seus programas.

Problemas causados por fatores fora do controle do programa geralmente devem ser previstos. Às vezes, quando o problema pode ser tratado localmente, valores de retorno especiais são uma boa forma de acompanhá-los. Caso contrário, exceções podem ser preferíveis.

Lançar uma exceção faz com que a pilha de chamadas seja desempilhada até o próximo bloco try/catch ou até o fim da pilha. O valor da exceção será passado para o bloco catch que a capturar, o qual deve verificar se ela é realmente do tipo esperado e então agir sobre ela. Para ajudar a lidar com o fluxo de controle imprevisível causado por exceções, blocos finally podem ser usados para garantir que um trecho de código sempre seja executado quando um bloco termina.

Exercícios

Tentar novamente

Suponha que você tenha uma função primitiveMultiply que, em 20% dos casos, multiplica dois números e, nos outros 80%, lança uma exceção do tipo MultiplicatorUnitFailure. Escreva uma função que envolva essa função problemática e simplesmente continue tentando até que uma chamada tenha sucesso, momento em que deve retornar o resultado.

Certifique-se de tratar apenas as exceções que você pretende tratar.

class MultiplicatorUnitFailure extends Error {}

function primitiveMultiply(a, b) {
  if (Math.random() < 0.2) {
    return a * b;
  } else {
    throw new MultiplicatorUnitFailure("Klunk");
  }
}

function reliableMultiply(a, b) {
  // Your code here.
}

console.log(reliableMultiply(8, 8));
// → 64
Mostrar dicas...

A chamada para primitiveMultiply deve acontecer dentro de um bloco try. O bloco catch correspondente deve relançar a exceção quando ela não for uma instância de MultiplicatorUnitFailure e garantir que a chamada seja repetida quando for.

Para fazer as tentativas, você pode usar um loop que só pare quando uma chamada tiver sucesso — como no exemplo look visto anteriormente neste capítulo — ou usar recursão e torcer para não obter uma sequência de falhas tão longa que estoure a pilha (o que é bem improvável).

A caixa trancada

Considere o seguinte objeto (um tanto artificial):

const box = new class {
  locked = true;
  #content = [];

  unlock() { this.locked = false; }
  lock() { this.locked = true;  }
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this.#content;
  }
};

É uma caixa com uma trava. Há um array dentro dela, mas você só pode acessá-lo quando a caixa estiver destrancada.

Escreva uma função chamada withBoxUnlocked que receba uma função como argumento, destranque a caixa, execute a função e, em seguida, garanta que a caixa seja trancada novamente antes de retornar, independentemente de a função ter retornado normalmente ou lançado uma exceção.

const box = new class {
  locked = true;
  #content = [];

  unlock() { this.locked = false; }
  lock() { this.locked = true;  }
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this.#content;
  }
};

function withBoxUnlocked(body) {
  // Your code here.
}

withBoxUnlocked(() => {
  box.content.push("gold piece");
});

try {
  withBoxUnlocked(() => {
    throw new Error("Pirates on the horizon! Abort!");
  });
} catch (e) {
  console.log("Error raised: " + e);
}
console.log(box.locked);
// → true

Para pontos extras, garanta que, se você chamar withBoxUnlocked quando a caixa já estiver destrancada, ela permaneça destrancada.

Mostrar dicas...

Este exercício pede um bloco finally. Sua função deve primeiro destrancar a caixa e então chamar a função passada como argumento dentro de um bloco try. O bloco finally depois disso deve trancar a caixa novamente.

Para garantir que não trancaremos a caixa quando ela não estava trancada inicialmente, verifique o estado da trava no início da função e só destranque/tranque quando ela começou trancada.