Promessas de JavaScript: uma introdução

As promessas simplificam os cálculos adiados e assíncronos. Uma promessa representa uma operação que ainda não foi concluída.

Jake Archibald
Jake Archibald

Desenvolvedores, preparem-se para um momento crucial na história do desenvolvimento web.

[Rufam os tambores]

As promessas chegaram ao JavaScript!

[Fogos de artifício explodem, papel brilhante cai do céu, a multidão vai à loucura]

Neste ponto, você se enquadra em uma destas categorias:

  • As pessoas estão comemorando ao seu redor, mas você não sabe o motivo de tanta animação. Talvez você nem tenha certeza do que é uma "promessa". Você daria de ombros, mas o peso do papel brilhante está pesando sobre seus ombros. Se sim, não se preocupe. Levei muito tempo para entender por que eu deveria me importar com isso. É recomendável começar do início.
  • Você dá um soco no ar! Já estava na hora, né? Você já usou essas Promises antes, mas se incomoda com o fato de que todas as implementações têm uma API um pouco diferente. Qual é a API da versão oficial do JavaScript? É recomendável começar com a terminologia.
  • Você já sabia disso e zomba de quem está pulando de alegria como se fosse novidade. Aproveite o momento para se orgulhar da sua superioridade e acesse diretamente a referência da API.

Suporte ao navegador e polyfill

Browser Support

  • Chrome: 32.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 8.

Source

Para que os navegadores que não têm uma implementação completa de promessas estejam em conformidade com as especificações ou para adicionar promessas a outros navegadores e ao Node.js, confira o polyfill (2k gzipped).

What's all the fuss about?

O JavaScript é de linha de execução única, o que significa que dois bits de script não podem ser executados ao mesmo tempo. Eles precisam ser executados um após o outro. Nos navegadores, o JavaScript compartilha uma linha de execução com uma série de outras coisas que variam de navegador para navegador. Mas, normalmente, o JavaScript está na mesma fila que a pintura, a atualização de estilos e o processamento de ações do usuário, como destacar texto e interagir com controles de formulário. A atividade em uma dessas coisas atrasa as outras.

Como ser humano, você é multithread. Você pode digitar com vários dedos, pode dirigir e conversar ao mesmo tempo. A única função de bloqueio com que precisamos lidar é o espirro, em que todas as atividades atuais precisam ser suspensas durante o espirro. Isso é bem chato, especialmente quando você está dirigindo e tentando conversar. Você não quer escrever um código que seja "espirro".

Você provavelmente já usou eventos e callbacks para contornar isso. Aqui estão os eventos:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

Isso não é nada espirrento. Recebemos a imagem, adicionamos alguns listeners e, em seguida, o JavaScript pode interromper a execução até que um desses listeners seja chamado.

Infelizmente, no exemplo acima, é possível que os eventos tenham acontecido antes de começarmos a detectá-los. Por isso, precisamos contornar isso usando a propriedade "complete" das imagens:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

Isso não captura imagens que apresentaram erro antes de termos a chance de detectá-las. Infelizmente, o DOM não oferece uma maneira de fazer isso. Além disso, estamos carregando uma imagem. As coisas ficam ainda mais complexas se quisermos saber quando um conjunto de imagens foi carregado.

Os eventos nem sempre são a melhor opção

Os eventos são ótimos para coisas que podem acontecer várias vezes no mesmo objeto: keyup, touchstart etc. Com esses eventos, você não se importa com o que aconteceu antes de anexar o listener. Mas, quando se trata de sucesso/falha assíncrona, o ideal é algo como:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

É isso que as promessas fazem, mas com uma nomenclatura melhor. Se os elementos de imagem HTML tivessem um método "ready" que retornasse uma promessa, poderíamos fazer o seguinte:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

Em sua forma mais básica, as promessas são um pouco como listeners de eventos, exceto que:

  • Uma promessa só pode ser bem-sucedida ou falhar uma vez. Ele não pode ser bem-sucedido ou falhar duas vezes, nem pode mudar de sucesso para falha ou vice-versa.
  • Se uma promessa for bem-sucedida ou falhar e você adicionar um callback de sucesso/falha mais tarde, o callback correto será chamado, mesmo que o evento tenha ocorrido antes.

Isso é extremamente útil para sucesso/falha assíncrona, porque você não está tão interessado no momento exato em que algo ficou disponível, mas sim em reagir ao resultado.

Terminologia de promessa

Domenic Denicola revisou o primeiro rascunho deste artigo e me deu nota "F" em terminologia. Ele me deixou de castigo, me forçou a copiar Estados e Destinos 100 vezes e escreveu uma carta preocupada para meus pais. Apesar disso, ainda confundo muitos termos, mas aqui está o básico:

Uma promessa pode ser:

  • fulfilled: a ação relacionada à promessa foi bem-sucedida.
  • rejeitada: a ação relacionada à promessa falhou.
  • pending: ainda não foi atendido ou rejeitado.
  • settled: foi atendida ou rejeitada.

A especificação também usa o termo thenable para descrever um objeto que é semelhante a uma promessa, porque tem um método then. Esse termo me lembra o ex-técnico de futebol da Inglaterra, Terry Venables, então vou usá-lo o mínimo possível.

As promessas chegaram ao JavaScript!

As promessas já existem há algum tempo na forma de bibliotecas, como:

As promessas acima e do JavaScript compartilham um comportamento comum e padronizado chamado Promises/A+. Se você é usuário do jQuery, eles têm algo semelhante chamado Deferreds. No entanto, os Deferreds não são compatíveis com Promise/A+, o que os torna sutilmente diferentes e menos úteis. Portanto, tenha cuidado. O jQuery também tem um tipo de Promise, mas é apenas um subconjunto de Deferred e tem os mesmos problemas.

Embora as implementações de promessa sigam um comportamento padronizado, as APIs gerais delas são diferentes. As promessas do JavaScript são semelhantes à API RSVP.js. Veja como criar uma promessa:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

O construtor de promessa usa um argumento, um callback com dois parâmetros, resolve e reject. Faça algo no callback, talvez assíncrono, e chame resolve se tudo funcionar. Caso contrário, chame reject.

Como throw no JavaScript antigo, é comum, mas não obrigatório, rejeitar com um objeto Error. O benefício dos objetos de erro é que eles capturam um stack trace, tornando as ferramentas de depuração mais úteis.

Veja como usar essa promessa:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() usa dois argumentos: um callback para um caso de sucesso e outro para um caso de falha. Ambos são opcionais, então você pode adicionar um callback apenas para o caso de sucesso ou falha.

As promessas do JavaScript começaram no DOM como "Futures", foram renomeadas para "Promises" e, por fim, movidas para o JavaScript. Ter esses recursos em JavaScript em vez do DOM é ótimo porque eles estarão disponíveis em contextos JS que não são do navegador, como Node.js (se eles os usam nas APIs principais é outra questão).

Embora sejam um recurso do JavaScript, o DOM não tem medo de usá-las. Na verdade, todas as novas APIs DOM com métodos assíncronos de sucesso/falha usarão promessas. Isso já está acontecendo com Quota Management, Font Load Events, ServiceWorker, Web MIDI, Streams e muito mais.

Compatibilidade com outras bibliotecas

A API de promessas do JavaScript trata qualquer coisa com um método then() como semelhante a uma promessa (ou thenable na linguagem de promessas suspiro). Portanto, se você usar uma biblioteca que retorna uma promessa Q, tudo bem, ela vai funcionar bem com as novas promessas do JavaScript.

No entanto, como mencionei, os Deferreds do jQuery são um pouco… inúteis. Felizmente, é possível convertê-los em promessas padrão, o que vale a pena fazer assim que possível:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

Aqui, o $.ajax do jQuery retorna um Deferred. Como ele tem um método then(), o Promise.resolve() pode transformá-lo em uma promessa do JavaScript. No entanto, às vezes, os adiados transmitem vários argumentos para os callbacks, por exemplo:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

Já as promessas de JS ignoram todas as chamadas, exceto a primeira:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

Felizmente, isso geralmente é o que você quer ou, pelo menos, dá acesso ao que você quer. Além disso, o jQuery não segue a convenção de transmitir objetos de erro para rejeições.

Código assíncrono complexo simplificado

Certo, vamos programar algumas coisas. Digamos que queremos:

  1. Iniciar um ícone de carregamento para indicar o carregamento
  2. Busca um JSON para uma história, que fornece o título e os URLs de cada capítulo.
  3. Adicionar título à página
  4. Buscar cada capítulo
  5. Adicionar a matéria à página
  6. Parar o controle de rotação

… mas também informar ao usuário se algo deu errado no processo. Também queremos interromper o ícone de carregamento nesse ponto. Caso contrário, ele vai continuar girando, ficar tonto e colidir com outra interface.

Claro que você não usaria JavaScript para entregar uma história, já que a exibição como HTML é mais rápida, mas esse padrão é bastante comum ao lidar com APIs: várias buscas de dados e, em seguida, uma ação quando tudo estiver pronto.

Para começar, vamos buscar dados da rede:

Promisifying XMLHttpRequest

As APIs antigas serão atualizadas para usar promessas, se possível, de uma forma compatível com versões anteriores. XMLHttpRequest é um excelente candidato, mas, enquanto isso, vamos escrever uma função simples para fazer uma solicitação GET:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

Agora vamos usá-lo:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

Agora podemos fazer solicitações HTTP sem digitar XMLHttpRequest manualmente, o que é ótimo, porque quanto menos eu tiver que ver as letras concatenadas irritantes de XMLHttpRequest, mais feliz será minha vida.

Encadeamento

then() não é o fim da história. É possível encadear thens para transformar valores ou executar outras ações assíncronas uma após a outra.

Transformação de valores

É possível transformar valores simplesmente retornando o novo valor:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

Como exemplo prático, vamos voltar para:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

A resposta é JSON, mas no momento estamos recebendo como texto simples. Poderíamos alterar nossa função get para usar o JSON responseType, mas também poderíamos resolver isso no ambiente de promessas:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

Como JSON.parse() usa um único argumento e retorna um valor transformado, podemos criar um atalho:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

Na verdade, podemos criar uma função getJSON() com muita facilidade:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() ainda retorna uma promessa, que busca um URL e analisa a resposta como JSON.

Como colocar ações assíncronas em fila

Também é possível encadear thens para executar ações assíncronas em sequência.

Quando você retorna algo de um callback then(), é um pouco mágico. Se você retornar um valor, o próximo then() será chamado com esse valor. No entanto, se você retornar algo semelhante a uma promessa, o próximo then() vai aguardar e só será chamado quando essa promessa for resolvida (com sucesso/falha). Exemplo:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

Aqui, fazemos uma solicitação assíncrona para story.json, que nos dá um conjunto de URLs para solicitar. Em seguida, solicitamos o primeiro deles. É aí que as promessas começam a se destacar dos padrões de callback simples.

Você pode até criar um método de atalho para acessar os capítulos:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

Não fazemos o download de story.json até que getChapter seja chamado, mas nas próximas vezes que getChapter for chamado, reutilizaremos a promessa da história, então story.json será buscado apenas uma vez. Viva as promessas!

Tratamento de erros

Como vimos anteriormente, then() usa dois argumentos, um para sucesso e outro para falha (ou fulfill e reject, na linguagem de promessas):

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

Você também pode usar catch():

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

Não há nada de especial em catch(), é apenas uma forma mais simples de then(undefined, func), mas é mais legível. Os dois exemplos de código acima não se comportam da mesma forma. O segundo é equivalente a:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

A diferença é sutil, mas extremamente útil. As rejeições de promessa avançam para o próximo then() com um callback de rejeição (ou catch(), já que é equivalente). Com then(func1, func2), func1 ou func2 será chamado, nunca ambos. Mas, com then(func1).catch(func2), ambos serão chamados se func1 rejeitar, já que são etapas separadas na cadeia. Tome o seguinte:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

O fluxo acima é muito semelhante ao try/catch normal do JavaScript. Os erros que ocorrem em um "try" vão imediatamente para o bloco catch(). Veja o fluxograma acima (porque eu adoro fluxogramas):

Siga as linhas azuis para promessas que são cumpridas ou as vermelhas para as que são rejeitadas.

Exceções e promessas de JavaScript

As rejeições acontecem quando uma promessa é explicitamente rejeitada, mas também implicitamente se um erro é gerado no callback do construtor:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

Isso significa que é útil fazer todo o trabalho relacionado à promessa dentro do callback do construtor de promessas, para que os erros sejam capturados automaticamente e se tornem rejeições.

O mesmo vale para erros gerados em callbacks then().

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

Tratamento de erros na prática

Com nossa história e capítulos, podemos usar catch para mostrar um erro ao usuário:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

Se a busca de story.chapterUrls[0] falhar (por exemplo, http 500 ou usuário off-line), ela vai pular todos os callbacks de sucesso seguintes, incluindo o de getJSON(), que tenta analisar a resposta como JSON, e também o callback que adiciona chapter1.html à página. Em vez disso, ele passa para o callback catch. Como resultado, "Falha ao mostrar o capítulo" será adicionado à página se alguma das ações anteriores falhar.

Como o try/catch do JavaScript, o erro é capturado e o código subsequente continua, então o ícone de carregamento sempre fica oculto, que é o que queremos. O exemplo acima se torna uma versão assíncrona não bloqueante de:

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

Talvez você queira usar catch() apenas para fins de registro, sem se recuperar do erro. Para fazer isso, basta gerar o erro novamente. Podemos fazer isso no método getJSON():

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

Conseguimos buscar um capítulo, mas queremos todos. Vamos fazer isso acontecer.

Paralelismo e sequenciamento: o melhor dos dois mundos

Pensar de forma assíncrona não é fácil. Se você estiver com dificuldades para começar, tente escrever o código como se ele fosse síncrono. Neste caso:

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

Pode ser! Mas é síncrono e bloqueia o navegador enquanto os downloads são feitos. Para que isso funcione de forma assíncrona, usamos then() para que as coisas aconteçam uma após a outra.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

Mas como podemos percorrer os URLs dos capítulos e buscá-los em ordem? Não funciona:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach não reconhece assincronia, então nossos capítulos apareceriam em qualquer ordem em que fossem baixados, que é basicamente como Pulp Fiction foi escrito. Isso não é Pulp Fiction, então vamos corrigir.

Como criar uma sequência

Queremos transformar nossa matriz chapterUrls em uma sequência de promessas. Podemos fazer isso usando then():

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

Esta é a primeira vez que vemos Promise.resolve(), que cria uma promessa que é resolvida para qualquer valor que você fornecer. Se você transmitir uma instância de Promise, ela será simplesmente retornada (observação: essa é uma mudança na especificação que algumas implementações ainda não seguem). Se você transmitir algo semelhante a uma promessa (com um método then()), ele vai criar uma Promise genuína que é cumprida/rejeitada da mesma forma. Se você transmitir qualquer outro valor, por exemplo, Promise.resolve('Hello'), ele vai criar uma promessa que será cumprida com esse valor. Se você chamá-la sem valor, como acima, ela será atendida com "undefined".

Há também Promise.reject(val), que cria uma promessa que rejeita com o valor fornecido (ou indefinido).

Podemos organizar o código acima usando array.reduce:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

Isso faz o mesmo que o exemplo anterior, mas não precisa da variável "sequence" separada. O callback de redução é chamado para cada item na matriz. "sequence" é Promise.resolve() na primeira vez, mas para o restante das chamadas, "sequence" é o que retornamos da chamada anterior. array.reduce é muito útil para reduzir uma matriz a um único valor, que, neste caso, é uma promessa.

Vamos juntar tudo:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

E pronto, temos uma versão totalmente assíncrona da versão síncrona. Mas podemos fazer melhor. No momento, nossa página está sendo baixada assim:

Os navegadores são muito bons em baixar várias coisas ao mesmo tempo, então estamos perdendo desempenho ao baixar os capítulos um após o outro. O que queremos fazer é baixar todos ao mesmo tempo e processá-los quando todos chegarem. Felizmente, existe uma API para isso:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all usa uma matriz de promessas e cria uma promessa que é cumprida quando todas são concluídas. Você recebe uma matriz de resultados (seja qual for o resultado das promessas) na mesma ordem das promessas que você transmitiu.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

Dependendo da conexão, isso pode ser segundos mais rápido do que carregar um por um, além de exigir menos código do que nossa primeira tentativa. Os capítulos podem ser baixados em qualquer ordem, mas aparecem na tela na ordem certa.

No entanto, ainda podemos melhorar o desempenho percebido. Quando o capítulo um chegar, vamos adicioná-lo à página. Isso permite que o usuário comece a ler antes que o restante dos capítulos chegue. Quando o capítulo três chegar, não vamos adicioná-lo à página porque o usuário pode não perceber que o capítulo dois está faltando. Quando o capítulo dois chegar, podemos adicionar os capítulos dois e três, etc.

Para fazer isso, buscamos o JSON de todos os capítulos ao mesmo tempo e criamos uma sequência para adicioná-los ao documento:

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

E pronto, o melhor dos dois mundos! Leva o mesmo tempo para entregar todo o conteúdo, mas o usuário recebe o primeiro conteúdo mais cedo.

Neste exemplo simples, todos os capítulos chegam mais ou menos ao mesmo tempo, mas o benefício de exibir um por vez será maior com mais capítulos maiores.

Fazer o mesmo com callbacks ou eventos no estilo Node.js (link em inglês) exige o dobro de código, mas, o mais importante, não é tão fácil de acompanhar. No entanto, as promessas não param por aí. Quando combinadas com outros recursos do ES6, elas ficam ainda mais fáceis.

Rodada bônus: recursos ampliados

Desde que escrevi este artigo, a capacidade de usar Promises aumentou muito. Desde o Chrome 55, as funções assíncronas permitem que o código baseado em promessas seja escrito como se fosse síncrono, mas sem bloquear a linha de execução principal. Leia mais sobre isso no meu artigo sobre funções assíncronas. Há suporte amplo para Promises e funções assíncronas nos principais navegadores. Confira os detalhes na referência da MDN sobre Promise e função assíncrona.

Agradecemos a Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans e Yutaka Hirano, que revisaram este documento e fizeram correções/recomendações.

Além disso, agradecemos a Mathias Bynens por atualizar várias partes do artigo.