Promises no JavaScript
Promises tratam-se de uma novidade (que já nem é mais tão novidade assim) no JavaScript que você deveria dominar assim que puder!
Este artigo é parte da série sobre JavaScript Assíncrono.
Promises nos ajudam a trabalhar com código assíncrono de uma maneira muito mais organizada.
Especificação
A especificação é "direta e reta".
let p = new Promise (function (resolve, reject) { | |
// resolva ou rejeite | |
}); |
Promises aceitam uma função por parâmetro. Esta função será executada recebendo duas novas funções em seus argumentos - uma para resolver a promise, outra para rejeitá-la. Caso um erro aconteça dentro de uma promise, ela também será rejeitada.
Uma promise implementa a interface thenable e, por isso, possui dois métodos principais:
then
: Executado quando a promise é resolvida.catch
: Executado em caso de rejeição (ou throw).
Quando resolvemos uma promise, podemos passar por parâmetro um valor que será recebido no método then. Da mesma forma, ao rejeitarmos uma promise podemos passar um motivo para tal, um valor conhecido como reason
.
Tanto a função resolve quanto a reject podem ser interpretadas como callbacks, elas não possuem um "this" definido e podem ser executados apenas uma vez dentro de uma promise.
Já o método then pode ser usado várias vezes para a mesma promise.
Caso uma promise já tenha sido resolvida, qualquer chamada a seu then resolverá com o mesmo resultado imediatamente.
Enquanto a promise está sendo executada (ainda esperando por ser resolvida ou rejeitada), ela encontra-se em estado pending
(pendente). Para, então, passar ao estado rejected
(rejeitada) ou resolved
(resolvida).
A especificação de promises encontra-se descrita na página de definição das Promises pelo TC39. Esta é uma especificação já bem definida e também amplamente implementada e suportada pelos principais navegadores, como pode ser visto na imagem abaixo (fonte: https://caniuse.com/#search=Promises).
Reject vs Throw
A ideia é que rejeitar uma promise signifique que o resultado dela não seja o esperado, mas isso é algo diferente de lançar um erro, como Dr. Axel Rauschmayer menciona em seu artigo.
Outra diferença importante é que, ao lançar um erro, o fluxo do código é interrompido e as instruções seguintes não serão executadas, ao passo que, ao executarmos a reject, ela rejeitará sua promise (como em uma callback de erro), mas continuará a executar seu código. Observe o exemplo abaixo:
new Promise(function(resolve, reject) { | |
console.log('A'); // será executado reject(); | |
// irá rejeitar a promise | |
console.log('B'); // será executado | |
throw(new Error('Falhou')); // lança uma excpetion | |
console.log('C'); // não será executado | |
}); |
Conceitualmente, você deveria lançar um erro quando uma exceção "esperada" for encontrada. Por exemplo, sua função esperava um parâmetro do tipo String e recebeu um objeto. Da mesma forma, uma promise irá rejeitar o que - assincronamente - atingiu um resultado inesperado, como uma falha a uma requisição ou ao tentar salvar algum dado utilizando a Cache API, por exemplo.
Encadeando
Uma característica que torna o uso de promises ainda mais interessante é o encadeamento.
Uma promise pode receber em seu resolve uma outra promise. Esta, será resolvida então, somente quando a segunda promise for resolvida.
function getAddress () { | |
return new Promise ((resolve, reject) =>{ | |
setTimeout(resolve, 2000); }); | |
} | |
function getUser () { | |
return new Promise((resolve, reject) =>{ | |
setTimeout(_=> resolve(getAddress()), 1000); | |
}); | |
} | |
getUser() | |
.then(result => console.log('Done')); |
Na última linha deste exemplo, ao chamarmos a função getUser
, ela retornará imediatamente uma promise com status pending, e seu then somente será executado daqui a 3 segundos, ou seja, depois que ambas as promises (getAddress
e getUser
) forem resolvidas.
Métodos estáticos
Além disso, promises têm também alguns métodos estáticos úteis, tais como: all: Promise.all([promise1, promise2, ...])
.
Este método retorna uma promise pendente que será resolvida quando todas as promises passadas por parâmetro (em uma array) forem resolvidas.
Ela será rejeitada se qualquer uma das promises rejeitar.
Promise.all([ | |
new Promise((resolve, reject) => {}), | |
new Promise((resolve, reject)=>{}), | |
new Promise((resolve, reject)=>{}) | |
]) | |
.then(result => { | |
console.info('Ok'); | |
}) | |
.catch(reason => { | |
console.warn('Failed!', reason); | |
}); |
Neste exemplo, a chamada à função em then acontecerá somente quando todas as promises na array tiverem sido resolvidas.
A função em catch será chamada caso qualquer uma delas falhe.
race: Promise.race([promise1, promise2, ...])
: também retorna uma promise pendente, mas ela será resolvida assim que qualquer uma das promises enviadas seja resolvida, ou seja, assim que a primeira delas resolver. Mas, se uma delas falhar antes de qualquer uma ser resolvida, então essa promise será rejeitada.
Promise.race([ | |
new Promise((resolve, reject)=>{}), | |
new Promise((resolve, reject)=>{}), | |
new Promise((resolve, reject)=>{}) | |
]) | |
.then(result => { | |
console.info('Ok'); | |
}) | |
.catch(reason=>{ | |
console.warn('Failed!', reason); | |
}); |
Já neste exemplo, a função em then será evocada assim que a primeira promise resolver e as demais serão ignoradas. Note que as demais promises não serão interrompidas, ou seja, suas instruções continuarão normalmente, mas o then será chamado uma única vez, e será apenas para a primeira que resolver. Enquanto que o catch neste caso, será chamado caso uma promise falhe antes de qualquer outra promise resolver. Vamos lá, exercite um pouco! Teste no console do seu navegador e brinque com os valores no setTimeout
e alterne entre resolve e reject de cada letra no exemplo abaixo.
Promise.race([ | |
new Promise((resolve, reject) => { | |
setTimeout(function() { | |
resolve('A'); | |
}, 500); | |
}), | |
new Promise((resolve, reject) => { | |
setTimeout(function(){ | |
resolve('B'); | |
}, 300); | |
}), | |
new Promise((resolve, reject) => { | |
setTimeout(function(){ | |
resolve('C'); | |
}, 500); | |
}), | |
new Promise((resolve, reject) => { | |
setTimeout(function() { | |
resolve('D'); | |
}, 500); | |
}), | |
new Promise((resolve, reject) => { | |
setTimeout(function(){ | |
resolve('E'); | |
}, 500); | |
}) | |
]) | |
.then(result => { | |
console.info(result); | |
}) | |
.catch(reason => { | |
console.warn('Failed: ', reason); | |
}); |
Concluindo As promises são muito úteis e nos ajudam a organizar e repensar nosso código, além de ser um padrão bem definido, pensado e testado. As novas APIs também já estão sendo todas definidas baseadas no uso de promises, veja o exemplo da fetch API ou da Cache API. Outra coisa muito legal é que podemos ver que o TC39 está ouvindo a comunidade ao evoluir esta feature. Por exemplo, a comunidade vem notando a necessidade de podermos cancelar promises, e a proposta para cancelable promises tem avançado. Acostume-se a usá-las no dia a dia e compreender como elas influenciam na evolução do próprio JavaScript, pois mais está por vir! O uso das promises é muito amplo e a linguagem tem evoluído nessa direção. Além disso, "spoiler alert" novas implementações usam as suas funções "promisificadas" para oferecer "n" novas opções!