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 = true
const 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 argumento
return // 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ônima
console.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 HTTP
  • statusText, 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)
})
// or
new 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.