3 coisas que você (provavelmente) não sabia sobre promises em JS
1. Promisificando o setTimeout
O setTimeout
, apesar de ser bastante utilizado, ainda conta com uma API callback-based, ou seja, somos obrigados a passar a continuação que será executada depois do timeout estipulado na forma de callback, o que por vezes pode ser inconveniente, principalmente se precisarmos encadear várias chamadas de setTimeout
:
// Famoso callback hell
setTimeout(() => {
// Primeiro timeout
// ...
setTimeout(() => {
// Segundo timeout
// ...
setTimeout(() => {
// Terceiro timeout
// ...
}, 1000);
}, 1000);
}, 1000);
No entanto, é possível criarmos uma "versão" do setTimeout
que é promise-based, isto é, que retorna uma promise que será resolvida após o timeout estipulado:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const main = async () => {
// Como o `wait` agora retorna uma promise,
// podemos utilizá-lo com async/await
await wait(1000);
// Primeiro timeout
// ...
await wait(1000);
// Segundo timeout
// ...
await wait(1000);
// Terceiro timeout
// ...
};
Neste implementação específica, o único caveat é que não é possível cancelar o timeout, dado que não temos acesso ao timer id retornado pelo setTimeout
.
Caso seja necessário cancelar o timeout, podemos uttilizar a seguinte implementação:
const wait = (ms) => {
let timeoutId;
const promise = new Promise((resolve) => {
timeoutId = setTimeout(resolve, ms);
});
const cancel = () => clearTimeout(timeoutId);
return {
promise,
cancel,
};
};
const main = async () => {
const { promise, cancel } = wait(1000);
// ...
cancel();
};
2. Resolvendo/rejeitando promises de forma programática
Por vezes, nós precisamos resolver ou rejeitar promises de forma programática, como por exemplo quando queremos testar o que acontece com algum componente ou parte da nossa aplicação quando uma promise está pendente.
A ideia consiste em extrair o resolver/rejecter da promise em questão para que então possamos chamá-los de forma programática em qualquer parte da aplicaçao:
let resolver;
let rejecter;
const promise = new Promise((resolve, reject) => {
resolver = resolve;
rejecter = reject;
});
// ... Em outro local da aplicação
resolver("Promise resolvida");
// Ou então
rejecter("Promise rejeitada");
Por exemplo, digamos que temos o seguinte componente React, que busca a cotação do dólar e exibe na tela:
export const ExchangeRate = () => {
const [exchangeRate, setExchangeRate] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const getExchangeRate = async () => {
setIsLoading(true);
try {
const exchangeRate = await fetchExchangeRate();
setExchangeRate(exchangeRate);
} catch {
alert("Erro ao buscar cotação");
}
setIsLoading(false);
};
return (
<div>
<button data-testid="exchange-rate-button" onClick={getExchangeRate}>
Buscar Cotação
</button>
{isLoading && <p data-testid="loading">Carregando...</p>}
{!isLoading && exchangeRate !== undefined && (
<p data-testid="exchange-rate">A cotação de hoje é: R${exchangeRate}</p>
)}
</div>
);
};
Para testar o comportamento do componente quando a promise está pendente, e, posteriormente, o que acontece quando ela resolve ou é rejeitada, podemos extrair o resolver da promise retornada pelo fetchExchangeRate
e chamá-los de forma programática:
let resolver;
jest.mock("./fetchExchangeRate", () => ({
fetchExchangeRate: () =>
new Promise((resolve) => {
// Extraindo resolver
resolver = resolve;
rejecter = reject;
}),
}));
describe("When user clicks on 'Buscar Cotação'", () => {
it("Displays a loading message and then displays the exchange rate", async () => {
render(<ExchangeRate />);
fireEvent.click(screen.getByTestId("exchange-rate-button"));
// Checa se o componente de loading está na tela
expect(screen.getByTestId("loading")).toBeInTheDocument();
// Resolve a promise
resolver(5.5);
expect(await screen.findByTestId("exchange-rate")).toHaveTextContent(
"A cotação de hoje é: R$5.5",
);
});
});
3. Encadeando continuações em promises já resolvidas/rejeitadas
Eu não sei você, mas quando eu comecei a mexer com promises em JS, uma das coisas com as quais eu me preocupava era a seguinte: "E se eu chamar then
em uma promise que já foi resolvida/rejeitada? O que acontece? Será que a continuação vai ser simplesmente engolida?".
Felizmente a resposta é não!
Uma das garantias que a promise nos dá é que as continuações que nós encadeamos nela serão sempre executadas, independentemente da promise já ter sido resolvida/rejeitada ou não.
Exemplo:
// Promise já resolvida
const promise = Promise.resolve(42);
promise.then(() => console.log(1));
promise.then(() => console.log(2));
promise.then(() => console.log(3));
// Logs:
// 1
// 2
// 3
// Promise já rejeitada
const otherPromise = Promise.reject(new Error("Oops"));
otherPromise.catch(() => console.log(1));
otherPromise.catch(() => console.log(2));
otherPromise.catch(() => console.log(3));
// Logs:
// 1
// 2
// 3
Demo: https://stackblitz.com/edit/typescript-mscxmt?file=index.ts
Outro
Se você tem interesse em fazer um mergulho mais profundo em promises e async de forma geral, dê uma olhada nesse repositório: https://github.com/henriqueinonhe/promises-training.
Ele contém uma série de exercícios práticos envolvendo promises, direcionados para quem já tem um conhecimento básico sobre o assunto mas quer se aprofundar.
Vale notar que cada exercício conta com uma bateria de testes que você pode rodar para verificar se a sua solução está correta ou não.