Javascript Pill — Cumprindo promessas com Javascript
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.readFile
do 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.com
e 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.
- O valor de retorno de uma função utilizada como argumento do método
then
é repassado a outrosthen
subsequentemente encadeados. - Se esse valor de retorno é uma Promise, o próximo
then
na cadeia recebe o valor de retorno dessa promise, quando resolvida com sucesso. - 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étodoaxios.post('http://bar.com', data)
produza alguma exeção, qualquer outrothen
subsequente é cancelado, e ocatch
é chamado. - 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));
// -> 10const 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 then
do Promise.all
, neste caso, seria um array com todos os data
de 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...
});
Concluindo
As Promises são um recurso poderoso do Javascript. Conhecê-las e dominá-las é uma habilidade útil no dia-a-dia de quem trabalha com a linguagem. Espero que tenham gostado do post e caso possuam alguma sugestão, crítica, etc. Fiquem à vontade :)
Referências