JavaScript

18 out, 2019

O futuro das Promises no JavaScript

Publicidade

Sempre que falamos de JavaScript acabamos caindo em uma das features mais importantes e poderosas da linguagem: as Promises.

Já falamos em alguns artigos sobre o que são Promises e até como você pode criar sua Promise do zero. Porém, o que quero mostrar nesta nossa próxima conversa é uma implementação que já comentei em meu artigo sobre “o que é uma Promise” e também uma das implementações mais importantes e mais aguardadas das próximas versões do nosso amado ECMA262. O Promise.allSettled.

Arrays de Promises

Você já deve ter se deparado com um caso similar a este: Imagine que você tem que realizar uma série de chamadas para APIs não relacionadas, uma das formas mais eficientes de se fazer tal atividade é simplesmente chamar todas ao mesmo tempo, certo? Assim vamos ter uma única execução paralela de todas as Promises que precisamos:


const promises = [ fetch('existe.html'), fetch('http://nao-existe.com') ]

O que vamos ter aqui é um array de Promises pendentes. Precisamos pegar o resultado de todas estas Promises, mas executando-as ao mesmo tempo, então o método mais comum seria o Promise.all:

const promises = [ fetch('existe.html'), fetch('http://nao-existe.com') 
]
const resultados = await Promise.all(promises)

Porém vamos cair em um grande problema. O Promise.all vai falhar e nos lançar uma exceção sempre que uma de nossas Promises sofrer um reject. Então não conseguiremos pegar o resultado da Promise que executou com sucesso somente porque uma das Promises do array retornou um erro.

Reflect

Até pouco tempo a solução padrão para isto era criar o que chamamos de uma “função de reflexão”, o que esta função faz é receber uma Promise como parâmetro, executá-la mas, ao invés de retornar seu resultado, ele retorna um objeto que é sempre resolvido, desta forma:

function reflect (promise) {
  return promise

    .then((valor) => ({ status: 'fulfilled', value: valor }))
  	.catch((razao) => ({ status: 'rejected', reason: razao }))
}

E então poderíamos fazer algo deste tipo:

const promises = [ fetch('existe.html'), fetch('http://nao-existe.com') 
]
const resultados = await Promise.all(promises.map(reflect))

Com isto, iríamos obter o seguinte resultado:

[
  { status: 'fulfilled', value: 'html do site' },
  { status: 'rejected', reason: 'not found' }
]

O que estamos essencialmente fazendo aqui é sobrescrever o comportamento original da Promise que enviamos para que ela retorne seu resultado dentro de um objeto que definirmos.

Promise.allSettled

O Promise.allSettled é uma proposta finalizada no TC39 que visa abstrair completamente todo o funcionamento da nossa função reflect, da seguinte maneira:

const promises = [ fetch('existe.html'), fetch('http://nao-existe.com') 
]
const resultados = await Promise.allSettled(promises)

Isto nos retornaria exatamente o que tivemos no resultado anterior utilizando o reflect.

De acordo com a implementação pelo V8 o método allSettled em Promises está disponível em todos os browsers maiores, apesar de que a lista de compatibilidade do MDN mostra algo diferente:

  • Chrome 76+
  • Firefox 71+
  • Safari 13+

E também na versão 12.9.0 do Node.js. Além disso é possível utilizar polyfills e transpilações do Babel para poder usar de forma mais abrangente

O Futuro das Promises

Estamos vendo uma mudança grande em relação ao landscape das Promises no JavaScript, principalmente no que diz respeito aos chamados“Promise Combinators”, que são estas funções do tipoPromise.all, que levam como parâmetro um array de Promises e retornam um valor único combinado sobre todas elas.

Atualmente nosso ecossistema consiste de apenas dois combinadores:

Promise.all: Executa todas as promises e causa umshort-circuitquando qualquer uma delas é rejeitada

Promise.race: Executa todas as promises e causa umshort-circuitquando qualquer uma delas é finalizada

Temos uma diferença entreresolvida– com a palavraresolved– efinalizada– em inglês,settled– que você pode ver no primeiro artigo da série sobre Promises que escrevi na seção “Estados de uma Promise”

A proposta futura para o ESNext é que tenhamos, além doPromise.allSettled– que deve sair na versão ES2020 – é um novo agregador chamadoPromise.any.

Promise.any

OPromise.anyé um caso bastante particular e interessante doPromise.race, no momento ele é uma proposta em estágio 3, o que significa que, pelo menos para o ES2020, provavelmente não estará disponível, mas que há grandes chances de ele aparecer no ES2021 ou futuros lançamentos.

Para podermos entender o que esta proposta significa, temos que entender o que oPromise.racefaz.

Em essência,Promise.racerecebe um array de Promises e retorna o resultado da primeira Promise deste array que for finalizada, seja ela uma rejeição ou resolução, ou seja, a primeira Promise que for executada – seja ela rejeitada ou resolvida – será retornada. Um caso de uso bastante comum para oPromise.raceé a criação de timeouts de execução:

const resultado = await Promise.race([
  algumaComputacaoPesadaQueRetornaUmaPromise(),
  setTimeoutQueRejeitaUmaPromise(2000)
])

O que vamos ter aqui é a execução das duas Promises em tempo real, se a computação finalizar antes do timeout, então a computação será retornada, senão o timeout rejeitará a Promise e não executará o processo computacional.

Outro caso bastante interessante é a busca do mesmo dado em diversos locais diferentes, por exemplo, a busca de um cálculo de frete utilizando diversos sistemas de cálculos, a solicitação é disparada para todos, o primeiro que retornar se torna o preço do frete.

Agora, o caso do Promise.any difere do race somente em uma palavra: ao invés de retornar a primeira Promisefinalizada, ele retorna a primeira Promiseresolvida. Ou seja, o caso particular que estamos falando é o oposto doPromise.all, oPromise.anysó irá rejeitar as Promises quandotodasas Promises de seu array foram rejeitadas.

Recentemente tive um caso de uso real para oPromise.any. Imagine que precisamos inserir um post em uma rede social feita para alunos em uma escola. Cada post possui um campoto, que o faz ser direcionado ou para uma turma, ou para um aluno ou então para toda a escola. Portanto, para que possamos inserir o post precisamos saber se uma dessas três entidades existe.

A lógica aqui é bastante simples: vamos disparar uma busca no banco de dados para os três casos, se pelo menos um dos três for resolvido, então nosso destino existe e podemos inserir o nosso post:

async function createPost (data) {
  try {
    await Promise.any([
      classService.findById(data.to),
      studentService.findById(data.to),
      schoolService.findById(data.to)
    ])
    const post = Post.create(new ObjectId(), data)
    await postRepository.save(post)
    return post
  } catch {
    throw new Error('Algo não existe')
  }
}

Nossos services retornam um erro do tipo EntityNotFoundError quando não conseguem encontrar uma entidade pelo seu ID, portanto, se uma das Promises for rejeitada, sabemos que será por conta deste erro, então todo o conteúdo da função será rejeitado.

Embora você possa utilizar o any como um polyfill implementado por alguns outros devs, a API oficial ainda não está disponível em nenhum browser, nem no Node.js. Só é possível utiliza-la através de transpilação pelo Babel.

O novo ecossistema

Com o any, nosso ecossistema de agregadores fica de seguinte forma:

  • Promise.all: Resolvido se todas as Promises são resolvidas, ou então é rejeitado se pelo menos uma delas for rejeitada
  • Promise.allSettled: Nunca rejeita, apenas resolve para uma lista de valores com os resultados de todas as Promises passadas
  • Promise.race: Resolvido com o valor da primeira Promise que for resolvida ou a razão da primeira Promise que for rejeitada
  • Promise.any: Resolve quando pelo menos uma promise for resolvida, rejeitado quando todas as Promises são rejeitadas

Conclusão

Com o tempo o nosso ecossistema de Promises está ficando cada vez mais robusto e mais simples de se trabalhar, apesar de existirem contrapontos sobre como isto deve ser feito e se, de fato, essas implementações devem existir no core do JavaScript, o fato é que estes métodos tornam nossa vida muito mais simples e nosso código muito mais legível.

Até mais!