Entendendo JavaScript Promises
Introdução a Promises
Uma promise (promessa) é comumente definida como um proxy para um valor que eventualmente ficará disponível.
Promises são um jeito de lidar com código assíncrono, sem ficar preso em "inferno de callbacks" (callback hell).
Promises são parte da linguagem por anos (introduzidas e padronizadas no ES2015), e se tornaram mais integradas recentemente, com async e await no ES2017.
Funções async usam promises por baixo dos panos, então entender como promises funcionam é fundamental para entender como async e await funcionam.
Como promises funcionam, resumidamente
Uma vez que a promise tenha sido chamada, ela inicia em um estado pendente (pending). Isso significa que a função que a chamou continua executando, enquanto a promise estiver pendente até que se resolva, retornando à função que a invocou os dados que foram requisitados.
A promise criada vai eventualmente se encerrar em um estado resolvido (resolved), ou em um estado rejeitado (rejected), chamando as respectivas funções callback (then
e catch
) ao terminar.
Quais APIs JS usam promises?
Em adição ao seu código e o de bibliotecas, promises são usadas por padrão em APIs web modernas, como:
- Battery API
- Fetch API
- Service Workers
É improvável que você mesmo não esteja utilizando promises no JavaScript moderno, então vamos começar a mergulhar nelas.
Criando uma promise
A Promise API expõe um contrutor Promise, por onde você inicializa usando new Promise()
:
let done = trueconst isItDoneYet = new Promise((resolve, reject) => {if (done) {const workDone = 'Here is the thing I built'resolve(workDone)} else {const why = 'Still working on something else'reject(why)}})
Como você pode ver, a promise checa a constante global done
, e se for true, a promise vai para o estado resolved (uma vez que a callback resolve
tenha sido chamada); caso contrário, a callback reject
é executada, colocando a promise em um estado rejected. (Se nenhuma dessas funções é chamada, a promise permanece no estado pending).
Usando resolve
e reject
, podemos nos comunicar com a função invocadora dizendo qual estado a promise estava, e o que fazer com isso. No caso acima nós só retornamos uma string, mas poderia ser um objeto, ou mesmo null
. Só de termos criado a promise no trecho acima, ela já começou a executar. É importante entender o que está acontecendo na seção Consumindo uma promise logo abaixo.
Um exemplo mais comum que você pode encontrar pois aí é uma técnica chamada Promisifying (promissificar). Essa técnica é um jeito de poder usar uma função JavaScript clássica que recebe uma callback, e retornar uma promise:
const fs = require('fs')const getFile = (fileName) => {return new Promise((resolve, reject) => {fs.readFile(fileName, (err, data) => {if (err) {reject (err) // chamar `reject` vai fazer com que a promise falhe com ou sem o erro passado como argumentoreturn // e não queremos ir mais longe}resolve(data)})})}getFile('/etc/passwd').then(data => console.log(data)).catch(err => console.error(err))
Em versões recentes do Node.js, você não precisa fazer essa conversão manual para grande parte da API. Há uma função promisifying disponível no módulo util que fará isso por você, dado que a função que você esteja "promissificando" siga a assinatura correta.
Consumindo uma promise
Na última seção, nós introduzimos como uma promise é criada.
Agora vamos ver como uma promise pode ser consumida ou usada.
const isItDoneYet = new Promise(/* ... como acima ... */)//...const checkIfItsDone = () => {isItDoneYet.then(ok => {console.log(ok)}).catch(err => {console.error(err)})}
Rodar checkIfItsDone()
vai especificar funções para executar quando a promise isItDoneYet
resolver (na chamada then
) ou rejeitar (na chamada catch
).
Encadeando promises
Uma promise pode ser retornada para outra promise, criando uma cadeia de promises.
Um ótimo exemplo de encadeamento de promises é a Fetch API, que podemos usar para obter um recurso e enfileirar uma cadeia de promises para executar quando o recurso for obtido.
A Fetch API um mecanismo baseado em promise, e chamar fetch()
é equivalente a definir suas próprias promises usando new Promise()
.
Exemplo de encadeamento de promises
const status = response => {if (response.status >= 200 && response.status < 300) {return Promise.resolve(response)}return Promise.reject(new Error(response.statusText))}const json = response => response.json()fetch('/todos.json').then(status) // note que a função `status` é na verdade **chamada** aqui, e ela **retorna uma promise**.then(json) // da mesma forma, a única diferença aqui é que a função `json` retorna uma promise que é resolvida com `data`.then(data => { // ... por isso que `data` aparece aqui como primeiro parâmetro da função anônimaconsole.log('Request succeeded with JSON response', data)}).catch(error => {console.log('Request failed', error)})
Nesse exemplo, nós chamamos fetch()
para obter uma lita de items TODO do arquivo todos.json
localizado na raiz do domínio, e nós criamos uma cadeia de promises.
Rodanr fetch()
retorna uma response (resposta), que contém várias propriedades, e dentre elas as que usamos no exemplo:
status
, um valor numérico representando o código de status HTTPstatusText
, uma mensagem de status, que seráOK
se a requisição for bem-sucedida.
response
também tem um método json()
, que retorna uma promise que vai resolver com o conteúdo do body processado e transformado em JSON.
Então dadas essas promises, isso é o que acontece: a primeira promise na cadeia é a função que nós definimos, chamada status()
, que checa o status da response e se não for uma resposta bem-sucedida (entre 200 e 299), a promise é rejeitada.
Essa operação fará com que a cadeia de promises pule todas promises encadeadas e pule diretamente para o catch()
no final, logando o texto Request failed
e a mensagem de erro.
Se em vez disso obter suceddo, é chamada a função json()
que definimos. Desde que a promise anterior, quando bem-sucedida, tenha retornado o objeto response
, nós o obtemos como um input para a segunda promise.
Nesse caso, nós retornamos os dados processados em JSON, assim a terceira promise recebe o JSON diretamente:
.then((data) => {console.log('Request succeeded with JSON response', data)})
e nós simplesmente o logamos no console.
Tratando erros
No exemplo da seção anterior, nós tinhamos um catch
que foi adicionado na cadeia de promises.
Quando qualquer coisa na cadeia de promises falha e dispara um erro ou rejeita uma promise, o controle vai para o catch()
mais próximo na cadeia.
new Promise((resolve, reject) => {throw new Error('Error')}).catch(err => {console.error(err)})// ornew Promise((resolve, reject) => {reject('Error')}).catch(err => {console.error(err)})
Cascateando erros
Se dentro do catch()
você lançar um erro, você pode adicionar um segundo catch()
para tratá-lo, e assim por diante.
new Promise((resolve, reject) => {throw new Error('Error')}).catch(err => {throw new Error('Error')}).catch(err => {console.error(err)})
Orquestrando promises
Promise.all()
Se você precisa sincronizar promises diferentes, Promise.all()
te ajuda a definir uma lista de promises, e executa algo quando elas todas são resolvidas.
Examplo:
const f1 = fetch('/something.json')const f2 = fetch('/something2.json')Promise.all([f1, f2]).then(res => {console.log('Array of results', res)}).catch(err => {console.error(err)})
A sintaxe de atribuição por desestruturação do ES2015 permite que você também faça isso:
Promise.all([f1, f2]).then(([res1, res2]) => {console.log('Results', res1, res2)})
É claro que você não está limitado a só usar o fetch
, qualquer promise pode ser usada dessa forma.
Promise.race()
Promise.race()
roda quando a primeira das promises que você passar for resolvida, e roda a callback anexada apenas uma vez, com o resultado da primeira promise resolvida.
Exemplo:
const first = new Promise((resolve, reject) => {setTimeout(resolve, 500, 'first')})const second = new Promise((resolve, reject) => {setTimeout(resolve, 100, 'second')})Promise.race([first, second]).then(result => {console.log(result) // second})
Erros comuns
Uncaught TypeError: undefined is not a promise
Se você obter um erro Uncaught TypeError: undefined is not a promise
no console, certifique-se de usar new Promise()
em vez de apenas Promise()
.
UnhandledPromiseRejectionWarning
Isso significa que a promise que você chamou foi rejeitada, mas não tinha nenhum catch
preparado para tratar o erro. Adicione um catch
depois do then
causador do erro para tratá-lo propriamente.