Javascript Pill — Cumprindo promessas com Javascript

Leonardo Cavalcante
sysvale
Published in
6 min readAug 13, 2018

Motivação: lidando com códigos assíncronos like the old days

Em códigos Javascript mais antigos, funções assíncronas recebem callbacks aos quais são repassados seus resultados. É o caso do padrão comumente utilizado no Node.js, em que uma determinada função assíncrona recebe um callback, que por sua vez recebe como parâmetros um objeto de erro e os resultados da operação, em caso de sucesso. Um exemplo é a leitura e impressão do conteúdo de um arquivo no disco:

fs.readFile('pacoca.txt', 'utf8', function (err, res) {
if (err) {
console.error(err);
return;
}
console.log(res);
});

Outro caso comum é ter uma função assíncrona que recebe como argumentos funções de callback para casos de sucesso e falha da operação. Algo como

function oldDaysAsyncOne(successCallback, failureCallback) {
// ...
}

reaproveitar o resultado dessa função, em outras funções, produziria um código semelhante a

oldDaysAsyncOne((result) => {
oldDaysAsyncTwo(result, (resultTwo) => {
oldDaysAsyncThree(resultTwo, (resultThree) => {
oldDaysAsyncFour(resultThree, (finalResult) => {
// faz alguma coisa com o finalResult
}, failureCallback)
}, failureCallback)
}, failureCallback)
}, failureCallback);

Essa estrutura chamativa recebe o carinhoso apelido de CALLBACK HELL ou CALLBACK PYRAMID OF DOOM.

A utilização de Promises, por outro lado, produziria algo como

asyncOne()
.then(result => asyncTwo(result))
.then(resultTwo => asyncThree(resultTwo))
.then(resultThree => asyncFour(resultThree))
.then(finalResult => {
// faz alguma coisa com o finalResult
})
.catch(failureCallback);

ou mesmo

asyncOne()
.then(asyncTwo)
.then(asyncThree)
.then(asyncFour)
.then(finalResult => {
// faz alguma coisa com o finalResult
})
.catch(failureCallback);

A diferença fundamental aqui é que

Uma promise é um objeto retornado ao qual são anexados callbacks, ao invés desses callbacks serem passados diretamente a uma função.

É propósito desse post exclarecer como as Promises funcionam, mas somente a nível bastante introdutório.

Vale ressaltar que as Promises são parte da especificação do ES2015, sendo, agora, um recurso nativo do Javascript. Mais informações, como questões sobre compatibilidade, bibliotecas, etc., podem ser consultadas nas referências deste artigo ou na própria web (ora pois).

Criando Promises

Na maior parte do tempo, estaremos consumindo APIs baseadas em Promises, p. ex. utilizando uma biblioteca como a axios (um cliente HTTP baseado em Promises). A criação de Promises manualmente, porém, permanece útil por diversas razões, uma delas é fornecer wrappers para APIs antigas.

O código abaixo cria uma Promise arbitrária

const promise = new Promise((resolve, reject) => {
// faz algo aqui
});

O construtor das Promises recebe como parâmetro uma função executora que nos permite resolver ou rejeitar a Promise manualmente.

Por resolver entende-se: finalizar a operação assíncrona com algum resultado.

Por rejeitar entende-se: terminar a operação assíncrona devido a alguma falha.

Em um exemplo prático, podemos fazer do fs.readFiledo Node.js, uma função baseada em Promises, como no código abaixo

function readFilePromise(fileName, encoding) {
return new Promise((resolve, reject) => {
fs.readFile(fileName, encoding, (err, res) {
if (err) reject(err);
else resolve(res);
});
});
}

Nosso primeiro exemplo, então, se tornaria

readFilePromise('pacoca.txt', 'utf8')
.then(res => console.log(res))
.catch(err => console.error(err));

Promise Chaining

Uma das características mais interessantes sobre as Promises é o seu encadeamento. Ele faz com que a execução ordenada de tarefas assíncronas seja bastante intuitiva.

Neste caso, creio que seja mais claro trabalhar com um exemplo. Suponha que tenhamos de fazer uma requisição ao domínio http://foo.come que precisamos utilizar a resposta recebida em outra requisição, ao domínio http://bar.com. Teríamos um código semelhante ao mostrado abaixo.

axios.get('http://foo.com')
.then(response => response.data)
.then(data => axios.post('http://bar.com', data))
.then(response => {
// faz algo com a resposta
})
.catch(err => console.error(err));

Alguns pontos importantes são evidenciados acima.

  1. O valor de retorno de uma função utilizada como argumento do método then é repassado a outros then subsequentemente encadeados.
  2. Se esse valor de retorno é uma Promise, o próximo then na cadeia recebe o valor de retorno dessa promise, quando resolvida com sucesso.
  3. Se alguma exceção/falha ocorrer em algum dos métodos na cadeia, o controle é passado para o método catch . P. ex. caso o método axios.post('http://bar.com', data) produza alguma exeção, qualquer outro then subsequente é cancelado, e o catch é chamado.
  4. Os métodos then são executados na ordem expressa, um após o outro.

Observe que o fluxo do nosso código assíncrono se torna bastante linear e se assemelha ao tratamento de exeções de grande parte das Linguagens de programação. Isso se torna ainda mais evidente através do async/await introduzido no ES2017, porém não o discutiremos aqui.

Vale ressaltar que o encadeamento é possível mesmo após o catch , de modo que o código abaixo é perfeitamente possível

axios.get('http://foo.com')
.then(response => response.data)
.then(data => axios.post('http://bar.com', data))
.then(response => {
// faz algo com a resposta
})
.catch(err => console.error(err))
.then(() => console.log('Olhe mãe, após o catch!'));

Promise.resolve e Promise.reject

É possível criar Promises que são resolvidas ou rejeitadas com um determinado valor constante. Por exemplo

const value = 10;
const promiseTen = Promise.resolve(10);
promiseTen.then(value => console.log(value));
// -> 10
const rejectedPromise = Promise.reject(new Error('um erro'));rejectedPromise.catch(() => console.error('ocorreu um erro!'));
// -> ocorreu um erro!

Tais métodos são atalhos para os códigos abaixo

new Promise(resolve => resolve(value));
new Promise(reject => reject(new Error('um erro')));

Esse tipo de característica permite que valores constantes sejam abstraídos enquanto Promises, e que tanto valores derivados de operações assíncronas quanto valores constantes possam ser tratados da mesma forma, quando conveniente. Por exemplo, quando de posse de um valor que possa ser ou não uma Promise, é possível convertê-lo em uma.

Promise.all

O método Promise.all retorna uma Promise que resolve quando todas as Promises passadas como argumento resolvem, ou rejeita, quando a primeira delas é rejeitada. O valor então passado ao método then corresponde a um array com o valor de retorno de todas as Promises passadas como argumento.

Por exemplo, se tivermos uma lista de itens a serem enviados a um entry point de uma API que processa somente um item por vez e retorna um conjunto de valores associados, para cada item, podemos escrever algo como

const items = [ item0, ..., itemN ];
const apiEntryPoint = 'http://foo.com';
const requests = items.map(item => (
axios.post(apiEntryPoint, item)
));
Promise.all(requests).then((values) => {
// Faz qualquer coisa...
})
.catch((error) => {
// Faz qualquer tratamento de exceção...
});

Isso é muito expressivo! Convertemos um array de items em um array de requisições, que é resolvido com o valor combinado da resposta de todas elas!

Se quisermos somente o campo data da resposta de cada uma dessas chamadas a API, podemos mudar criação dorequests para algo como

const requests = items.map(item => (
axios.post(apiEntryPoint, item)
.then(response => response.data)
));

O resultado passado ao thendo Promise.all , neste caso, seria um array com todos os datade todas as requisições realizadas!

Promise.race

Pode ser o caso, em algum momento, de recuperar o valor da primeira Promise a ser resolvida, ou lançar uma exceção, assim que a primeira delas for rejeitada, dentre um conjunto de Promises. É o propósito do método Promise.race .

Se tivermos dois entry points diferentes, para a coleta dos mesmos dados, digamos http://foo.com e http://bar.com , podemos fazer duas requisições e pegar o primeiro resultado retornado, como no exemplo abaixo

const resquests = [
axios.get('http://foo.com'),
axios.get('http://bar.com'),
];
Promise.race(requests).then((response) => {
// faz qualquer coisa...
})
.catch((error) => {
// faz qualquer tratamento de erros...
});

--

--