ECMAScript 2015: Promises

ES2015 Gotchas é uma série quinzenal sobre as novidades e pegadinhas do ES2015.

Promises é o primeiro grande passo do Javascript para tornar nossa programação assíncrona cada vez mais simples e legível, afinal ninguém aguenta mais callbacks! Mas apesar dessa solução maravilhosa ter chegado até nós, parece que poucos realmente sabem como usar essa ferramenta, e ainda cometemos muitos erros básicos.

AVISO: este não é um artigo introdutório, caso ainda não tenha muita familiaridade com essa maravilha, sugiro que comece por outro artigo. Aviso dado, vamos nos divertir um pouco:

Não insista no Callback Hell

Toda vez que se perceber subindo mais um nível de tabulação no seu código pare e pense duas vezes, principalmente se já estiver dentro de uma promise.

remotedb.allDocs({  
  include_docs: true,
  attachments: true
}).then(function (result) {
  var docs = result.rows;
  docs.forEach(function(element) {
    localdb.put(element.doc).then(function(response) {
      console.log("Pulled doc with id ",  element.doc._id);
    }).catch(function (err) {
      if (err.status == 409) {
        localdb.get(element.doc._id).then(function (resp) {
          localdb.remove(resp._id, resp._rev).then(function (resp) {
            // NÃÃÃÃÃÃOOOOOOO...

Se fosse para continuar subindo níveis de callback seria melhor nem usar promises, simples assim, não faça isso! Ao trabalhar com promises devemos encadear elas e crescer verticalmente de maneira legível, com passos claros:

remotedb.allDocs(...)  
.then(function (resultOfAllDocs) {
  return localdb.put(...);
}).then(function (resultOfPut) {
  return localdb.get(...);
}).then(function (resultOfGet) {
  return localdb.put(...);
}).catch(function (err) {
  console.log(err);
});

Entenda o que esta passando para frente

O comportamento das promises mudam dependendo do que você retornar em cada passo, tenha ciência do que esta fazendo:

facaAlgo().then(function() {  
  facaOutraCoisa();
})
.then(function(arg) {
  console.log(arg); // undefined
  // nada foi retornado no passo anterior
  // esse passo foi iniciado sincronamente
  return 'VALOR';
})
.then(function(arg) {
  console.log(arg); // 'VALOR'
  // um valor foi retornado no passo anterior
  // esse passo foi iniciado sincronamente
  return Promise.resolve('PROMISE');
})
.then(function(arg) {
  console.log(arg); // 'PROMISE'
  // uma promise foi retornado no passo anterior
  // ela sera resolvida antes desse passo iniciar
  // seu resultado foi passado como argumento nesse passo
  // esse passo foi iniciado assincronamente
});

Tenha em mente que é possível também ter retornos diferentes em um mesmo passo.

Iterando dados assíncronos

Uma das primeiras barreiras para quem está começando com promises é como lidar com loopings e coleções executando ações assíncronas, dentro de outra promise, sem perder o encadeamento com as próximas:

// Bora remover todas as linhas de um documento:
db.allDocs({include_docs: true}).then(function (result) {  
  result.rows.forEach(function (row) {
    db.remove(row.doc);  
  });
}).then(function () {
  // E ingenuamente acreditamos que esse passo só será
  // executado depois que cada uma delas for excluída...
});

Para que o ultimo then() acima seja executado no momento certo, temos que retornar uma promise no passo anterior, que finalize apenas quando todos as promises do método remove() sejam completadas. Montar isso na mão seria bem chato, mas esse é um caso muito comum, que sendo assim merece uma solução nativa:

db.allDocs({include_docs: true}).then(function (result) {  
  return Promise.all(result.rows.map(function (row) {
    return db.remove(row.doc);
  }));
}).then(function (arrayDeResultados) {
  // Agora sim todas as linhas foram removidas!
});

Como vemos acima o objeto Promise tem alguns métodos auxiliares como o all() usado acima, que recebe um array de promises, retornando uma outra promise que será finalizada apenas depois que todas as que recebeu forem concluídas, ou será rejeitada caso qualquer uma falhe.

Esteja sempre prevenido

Na pressa do dia a dia podemos esquecer que promises podem falhar, independente do motivo. E caso não tratemos esses erros, podem chamar os caça fantasmas, pois vários surgirão perseguindo usuários em produção, com erros muito difíceis de serem replicados. O lado bom é que diferente dos callbacks comuns, podemos fazer esse tratamento uma única vez, tornando tudo muito mais simples:

minhaPromise().then(function () {  
  return outraPromise();
}).then(function () {
  return aindaOutraPromise();
}).catch(console.log.bind(console, 'Xii, deu ruim:'));

Conhece outros casos de uso de promises com resultados inusitados? Compartilhe com a gente!

Até a próxima! Fiquem ligados na série ES2015 Gotchas!

William Grasel

Read more posts by this author.