Promises: implementando timeout com Promise.race
A especificação ES2015 (ES6) adicionou o suporte a Promises, recurso poderoso para lidar com o resultado futuro de uma ação. Porém, o ES6 não trouxe nenhum mecanismo que permita cancelá-las automaticamente dentro de uma janela de tempo. Nesse artigo implementaremos o recurso de timeout em Promises.
O problema
Temos um simples HTML que importa o módulo js/app.js
utilizando o recurso de importação nativa de módulos disponível no Google Chrome. Se quiser saber mais sobre esse recurso, você pode consultar o artigo “Importação nativa de módulos no browser” deste mesmo autor.
Vamos ao HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>App</title>
</head>
<body>
<script type="module" src="js/app.js"></script>
</body>
</html>
Vejamos o código do módulo js/app.js
:
// js/app.js
import { fetchHandler } from './promise-util.js';
/*
Assim que o módulo é carregado a API é acessada
*/
const getNegociacoes = () =>
fetch('http://localhost:3000/negociacoes/semana')
.then(fetchHandler);
getNegociacoes()
.then(negotiations => {
console.log(negotiations);
alert('Operation complete!');
})
.catch(err => {
console.log(err);
alert('Operation failed!');
});
O módulo app.js
importa a função fetchHandler
do módulo promise-util.js
. Vejamos o conteúdo deste módulo:
// js/promise-util.js
export const fetchHandler = res => {
if(!res.ok) throw Error(res.statusText);
return res.json();
};
Essa função é utilizada para lidar com o código de status da resposta e também para convertê-la no formato JSON.
Excelente! Voltando nosso olhar para app.js
, vemos que a função fetchHandler
é utilizada pela API fetch
que busca as negociações da semana. Importante destacar que a API fetch
retorna uma Promise, razão pela qual pudemos encadear chamadas às funções then
e catch
.
Tudo muito bom, mas o que aconteceria se o usuário estivesse executando a operação em uma rede muita lenta e congestionada, por exemplo, uma rede 3G? Por mais que possamos exibir um ajax status para o usuário alertando-o que a operação ainda esta sendo executada, precisamos de alguma maneira definir um limite máximo de espera e, caso esse limite seja excedido, cancelamos a operação.
O problema é que o ES2015 não suporta Promises com timeout, diferente de bibliotecas como bluebird que aderem à especificação Promise adicionando novos recursos.
A boa notícia é que não há limites para um Cangaceiro JavaScript e neste artigo implementaremos um mecanismo de timeout através de Promise.race
. Aliás, este é o momento ideal para conhecermos mais sobre essa função.
Entendendo Promise.race
Temos duas promises criadas através de new Promise()
:
const promise1 = new Promise((resolve, reject) =>
setTimeout(() => resolve('promise 1 resolved'), 3000));
const promise2 = new Promise((resolve, reject) =>
setTimeout(() => resolve('promise 2 resolved'), 1000));
A primeira, só será resolvida depois de três segundos, já a segunda depois de um segundo. Todavia, queremos realizar uma “corrida” (race) entre as promises. Nessa corrida, só queremos o resultado daquela que for resolvida primeiro. É aí que entra Promise.race
.
A função Promise.race
recebe uma lista de promises e assim que uma delas for resolvida, receberemos imediatamente seu resultado na próxima chamada encadeada à then
. As demais promises são ignoradas:
// exemplo isolado
const promise1 = new Promise((resolve, reject) =>
setTimeout(() => resolve('promise 1 resolvida'), 3000));
const promise2 = new Promise((resolve, reject) =>
setTimeout(() => resolve('promise 2 resolvida'), 1000));
// vai exbir no console "promise 2 resolvida";
Promise.race([
promise1,
promise2
])
.then(result => console.log(result))
.catch(err => console.log(err));
É importante estar atento que qualquer rejeição que aconteça durante a resolução das Promises direcionará o fluxo da aplicação para dentro a função catch
. Em suma, estamos interessados no resultado da primeira promise resolvida, mas se algum erro acontecer antes de qualquer resultado válido, caímos dentro do catch
.
Vejam que Promise.race
é muito diferente de Promise.all
, esta última, retorna dentro de um array o resultado de todas as promises.
Uma das soluções para se implementar o timeout de promises é através do uso da função Promise.race
. Veremos a lógica que utilizaremos a seguir.
A lógica de timeout
Para termos uma Promise que suporte timeout, precisamos emparelhá-la com uma Promise que será rejeitada dentro de um tempo estipulável. Isso significa que se a nossa Promise demorar mais do que o tempo definido para a rejeição da outra Promise, automaticamente seremos direcionados para o catch
encadeado.
Chegou a hora de implementarmos nossa promiseWithTimeout
.
Criando uma Promise com timeout
Vamos alterar o arquivo js/promise-util.js
e adicionar a função promiseWithTimeout
. Ela recebe como primeiro parâmetro a Promise que desejamos executar e como segundo o timeout em milissegundos que desejamos definir como tempo limite da Promise:
// js/promise-util.js
export const fetchHandler = res => {
if(!res.ok) throw Error('Api error!');
return res.json();
};
export const promiseWithTimeout = (promise, milliseconds) => {
/*
Esta promise fará a rejeição da Promise dentro do tempo definido
em milliseconds
*/
const timeout = new Promise((resolve, reject) =>
setTimeout(() =>
reject(`Promise timeout reached (limit: ${milliseconds} ms)`),
milliseconds));
/*
Faz uma corrida entre a Promise que desejamos resolver com a promise
com o timeout programador
*/
return Promise.race([
timeout,
promise
]);
};
Agora, no módulo app.js
, importamos a função promiseWithTimeout
para que possamos utilizá-la:
// js/app.js
import { fetchHandler, promiseWithTimeout } from './promise-util.js';
const getNegociacoes = () =>
fetch ('http://localhost:3000/negociacoes/semana')
.then(fetchHandler);
/*
promiseWithTimeout recebe a promise da Fetch API e
define em 100ms (bem baixo) o limite máximo
*/
promiseWithTimeout(getNegociacoes(),100)
.then(negotiations => {
console.log(negotiations);
alert('Operation complete!');
})
.catch(err => {
console.log(err);
alert('Operation failed!');
});
Porém, como fazemos o teste? Precisamos simular uma conexão bem lenta! Isso é fácil de ser feito através do Chrome Dev Tools. Com a ferramenta aberta, na aba Network (Rede), há uma caixa de opção com o item online selecionado. Basta mudar para Slow 3G:
Essa mudança já é suficiente para vermos nossa promise sendo cancelada através de uma rejeição.
Conclusão
Por mais que Promises não suportem timeout, nada impede que o programador implemente essa funcionalidade utilizando o conhecimento que já possui. Outra alternativa é apelar para bibliotecas como bluebird
citadas neste artigo que já trazem a funcionalidade aqui implementada.
E você? Já precisou realizar o timeout em um Promise antes? Conte-nos como foi sua experiência.