Promessas de JavaScript: uma introdução

As promessas simplificam 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 da Web.

[Início dos tambores]

As promessas chegaram ao JavaScript!

[Fogos de artifício explodem, papel brilhante chove de cima, a multidão enlouquece]

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

  • As pessoas comemoram à sua volta, mas você não tem certeza do motivo dessa confusão. Talvez você não tenha certeza do que é uma "promessa". Você encolheria, mas o peso do papel brilhante é muito grande. Se sim, não se preocupe com isso, demorei uma eternidade para entender por que eu deveria me importar com essas coisas. É melhor começar do início.
  • Você soca o ar! Já tava na hora, não é? Você já usou esses itens de promessa antes, mas está incomodado que todas as implementações têm uma API um pouco diferente. Qual é a API da versão oficial do JavaScript? Comece com a terminologia.
  • Você já conhecia tudo e ridiculariza essas pessoas como se fosse novidade para elas. Aproveite um momento para conhecer sua superioridade e vá direto para a Referência da API.

Suporte a navegadores e polyfill

Compatibilidade com navegadores

  • 32
  • 12
  • 29
  • 8

Origem

Para adaptar os navegadores que não têm uma implementação completa de promessas à conformidade com as especificações ou adicionar promessas a outros navegadores e ao Node.js, confira o polyfill (2 mil arquivos compactados com gzip).

Por que todo esse confusão?

O JavaScript tem um único thread, ou seja, duas partes do script não podem ser executadas ao mesmo tempo. Elas precisam ser executadas uma após a outra. Em navegadores, o JavaScript compartilha uma linha de execução com muitas outras coisas que variam de um navegador para outro. 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 destaque de texto e interação com controles de formulários. A atividade em uma dessas coisas atrasa as outras.

Como ser humano, você usa várias linhas de execução. Você pode digitar com vários dedos, dirigir e conversar ao mesmo tempo. A única função bloqueadora com que temos de lidar é o espirro, em que todas as atividades em andamento precisam ser suspensas pela duração dele. Isso é muito irritante, especialmente quando você está dirigindo e tentando manter uma conversa. Você não quer escrever um código que espirre.

Você provavelmente usou eventos e callbacks para contornar isso. Veja os eventos:

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

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

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

Não há nenhum espirro. Obtemos a imagem, adicionamos alguns listeners e o JavaScript pode parar a execução até que um deles seja chamado.

Infelizmente, no exemplo acima, é possível que os eventos tenham acontecido antes de começarmos a ouvi-los. Portanto, 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 as imagens que apresentaram erros antes de começarmos a ouvi-las. Infelizmente, o DOM não permite fazer isso. Além disso, uma imagem é carregada. Tudo fica ainda mais complexo quando queremos saber quando um conjunto de imagens foi carregado.

Eventos nem sempre são a melhor maneira

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

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 nomes melhores. Se elementos de imagem HTML tivessem um método "ready" que retornasse uma promessa, poderíamos fazer isto:

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:

  • Uma promessa só pode ser bem-sucedida ou falhar uma vez. Ela não pode ser concluída ou falhar duas vezes, nem pode alternar entre sucesso e falha ou vice-versa.
  • Se uma promessa tiver sido concluída ou falhar e você adicionar um callback de sucesso/falha, o retorno correto será chamado, mesmo que o evento tenha ocorrido anteriormente.

Isso é extremamente útil para sucesso/falha assíncronos, porque você está menos interessado no momento exato em que algo ficou disponível e mais interessado em reagir ao resultado.

Terminologia das promessas

Domenic Denicola leu a primeira versão deste artigo e me atribuiu nota zero para terminologia. Ele me colocou em detenção, me obrigou a copiar Estados e destinos 100 vezes e escreveu uma carta preocupada para meus pais. Apesar disso, eu ainda me confundo com a terminologia, mas estes são os princípios básicos:

Uma promessa pode ser:

  • atendida: a ação relacionada à promessa foi bem-sucedida
  • rejected: a ação relacionada à promessa falhou
  • pendente: ainda não foi atendido ou rejeitado.
  • resolvido: foi concluído ou recusado

A especificação também usa o termo thenable para descrever um objeto semelhante à promessa, que tem um método then. Esse termo me lembra o ex-treinador de futebol inglês, Terry Venables, então, vou usá-lo o mínimo possível.

As promessas chegaram ao JavaScript!

As promessas 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ê é um usuário do jQuery, eles têm algo semelhante chamado Deferreds. No entanto, os diferidos não são compatíveis com Promise/A+, o que os torna sutilmente diferentes e menos úteis. Portanto, cuidado. O jQuery também tem um tipo de promessa, mas é apenas um subconjunto de Deferreds e apresenta os mesmos problemas.

Embora as implementações de promessas sigam um comportamento padronizado, existem diferenças entre as APIs gerais. As promessas do JavaScript são semelhantes na API ao RSVP.js. Confira 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 promessas recebe um argumento, um callback com dois parâmetros, é resolvido e rejeitado. Faça algo dentro do callback, talvez algo assíncrono, e chame "resolver" se tudo funcionar. Caso contrário, chame "rejeitar".

Assim como o throw no JavaScript simples, é comum, mas não obrigatório, rejeitar com um objeto Error. A vantagem dos objetos Error é 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 o caso de sucesso e outro para a falha. Ambos são opcionais, portanto, você pode adicionar um callback apenas para a sucesso ou a falha.

As promessas de JavaScript começaram no DOM como "Futures", foram renomeadas para "Promises" e, finalmente, movidas para o JavaScript. É ótimo tê-los em JavaScript, em vez de no DOM, porque eles estarão disponíveis em contextos JS fora do navegador, como Node.js, independentemente de serem usados ou não nas APIs principais.

Embora sejam um recurso JavaScript, o DOM não tem medo de usá-los. Na verdade, todas as novas APIs do DOM com métodos de sucesso/falha usarão promessas. Isso já está acontecendo com gerenciamento de cotas, eventos de carregamento de fonte, ServiceWorker, Web MIDI, Streams e muito mais.

Compatibilidade com outras bibliotecas

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

Embora, como já mencionei, os Deferreds do jQuery sejam um pouco... inúteis. Felizmente, é possível convertê-los em promessas padrão, o que é bom fazer o quanto antes:

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 de JavaScript. No entanto, às vezes os adiados passam 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 do JS ignoram todas, exceto a primeira:

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

Felizmente, é isso que você quer ou pelo menos dá acesso ao que você quer. Além disso, é importante lembrar que o jQuery não segue a convenção de passar objetos Error nas rejeições.

Simplificar o código assíncrono complexo

Vamos codificar algumas coisas. Digamos que você queira:

  1. Iniciar um ícone de carregamento para indicar o carregamento
  2. Buscar algum JSON para uma história, que nos dará o título e URLs para cada capítulo
  3. Adicionar título à página
  4. Buscar cada capítulo
  5. Adicionar a história à página
  6. Parar o ícone de carregamento

... mas também informar ao usuário se algo deu errado no caminho. Também é necessário parar o ícone de carregamento nesse ponto, caso contrário, ele continuará girando, ficará tonto e travará em alguma outra interface.

Obviamente, você não usaria JavaScript para enviar uma história, a disponibilização como HTML é mais rápida, mas esse padrão é muito comum ao lidar com APIs: várias buscas de dados e, depois, fazem algo quando tudo é concluído.

Para começar, vamos buscar dados da rede:

Promisificação de XMLHttpRequest

As APIs antigas serão atualizadas para usar promessas, se for possível de maneira compatível com versões anteriores. XMLHttpRequest é um candidato principal, mas, nesse meio tempo, 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á-la:

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

Agora, podemos fazer solicitações HTTP sem digitar manualmente XMLHttpRequest, o que é ótimo, porque, quanto menos eu precisar ver o irritante camel-case do XMLHttpRequest, mais feliz será minha vida.

Encadeamento

O then() não é o fim da história. Você pode encadear thens para transformar valores ou executar mais ações assíncronas, uma após a outra.

Transformar valores

Para transformar valores, basta retornar 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 o que estamos recebendo no momento é um texto simples. Podemos alterar nossa função "get" para usar o JSON responseType, mas também podemos resolvê-la com 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, é possível usar 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 enfileirar ações assíncronas

Você também pode encadear thens para executar ações assíncronas em sequência.

Quando você retorna algo de um callback then(), isso é um pouco mágico. Se você retornar um valor, o próximo then() será chamado com ele. No entanto, se você retornar algo semelhante a uma promessa, a próxima then() aguardará isso e será chamada apenas quando essa promessa for definida (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 fornece um conjunto de URLs a serem solicitados. Em seguida, solicitamos o primeiro deles. É aí que as promessas começam a se destacar dos simples padrões de callback.

Você pode até criar um método de atalho para conseguir 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 chamadas de getChapter, reutilizamos a promessa da história. Portanto, story.json será buscado apenas uma vez. Eba!

Tratamento de erros

Como vimos anteriormente, then() aceita dois argumentos, um para sucesso e outro para falha (ou atender e rejeitar, na terminologia 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 especial sobre catch(), é apenas o açúcar de then(undefined, func), mas é mais legível. Observe que os dois exemplos de código acima não se comportam da mesma forma. O último é 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 pulam para o próximo then() com um callback de rejeição (ou catch(), já que é equivalente). Com then(func1, func2), são chamadas func1 ou func2, mas nunca ambas. No entanto, com then(func1).catch(func2), ambas serão chamadas se func1 for rejeitada, porque são etapas separadas na cadeia. Considere 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(). Aqui está o fluxograma acima (eu adoro fluxogramas):

Siga as linhas azuis para promessas atendidas ou vermelhas para as rejeitadas.

Exceções e promessas do JavaScript

As rejeições acontecem quando uma promessa é rejeitada explicitamente, mas também implicitamente se um erro for 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 a promessas dentro do callback do construtor de promessas, para que os erros sejam detectados automaticamente e se tornarem 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);
})

Como lidar com erros na prática

Com nossa história e capítulos, podemos usar catch para exibir 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 o usuário está off-line), ela vai pular todos os callbacks de sucesso a seguir, incluindo o de getJSON(), que tenta analisar a resposta como JSON e também vai ignorar o callback que adiciona Capítulo1.html à página. Em vez disso, ela se move para o callback "catch". Como resultado, se qualquer uma das ações anteriores falhar, a mensagem "Failed to show Chapter" será adicionada à página.

Assim como o try/catch do JavaScript, o erro é capturado e o código subsequente continua. Portanto, o ícone de carregamento fica sempre oculto, que é o que queremos. O código acima se torna uma versão assíncrona e sem bloqueio do código:

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'

O catch() pode ser usado simplesmente para fins de geração de registros, sem recuperação do erro. Para fazer isso, basta gerar o erro novamente. Podemos fazer isso em nosso 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 eles. Vamos fazer isso acontecer.

Paralelismo e sequência: como aproveitar ao máximo ambos

Não é fácil pensar de maneira assíncrona. Se você está com dificuldades para começar, tente escrever o código como se 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'

Funcionou! Mas ele sincroniza e bloqueia o navegador durante os downloads. Para fazer isso de forma assíncrona, usamos then() para fazer com 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? Isso não funciona:

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

O forEach não reconhece assincronia. Portanto, nossos capítulos apareceriam na ordem de download, que é basicamente a forma como Pulp Ficção foi escrita. Como não estamos tratando de Puls, vamos corrigir o problema.

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 informado. Se você transmitir uma instância de Promise, ela vai simplesmente retorná-la. 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 (tem um método then()), ela vai criar uma Promise genuína que será atendida/rejeitada da mesma maneira. Se você transmitir qualquer outro valor, como Promise.resolve('Hello'), ele cria uma promessa que será atendida com esse valor. Se você a chamar sem valor, como mostrado acima, ela será atendida com "indefinido".

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

Podemos ordenar 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())

Esse código faz o mesmo que o exemplo anterior, mas não precisa da variável separada "sequência". Nosso callback reduzido é chamado para cada item na matriz. "sequência" é Promise.resolve() na primeira vez, mas para o restante das chamadas, "sequência" é o que retornamos da chamada anterior. array.reduce é muito útil para resumir uma matriz a um único valor, que, nesse caso, é uma promessa.

Vamos juntar tudo isso:

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 aí está, uma versão totalmente assíncrona da versão síncrona. Mas podemos fazer melhor. No momento, o download da nossa página acontece da seguinte forma:

Os navegadores são muito eficientes em fazer o download de vários itens ao mesmo tempo. Por isso, estamos perdendo desempenho ao fazer o download de capítulos um após o outro. Queremos fazer o download de todos ao mesmo tempo e processá-los quando isso acontecer. Felizmente, existe uma API para isso:

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

Promise.all usa uma matriz de promessas e cria uma promessa que será atendida quando todas forem concluídas com sucesso. Você recebe uma matriz de resultados (com os dados das promessas atendidas) na mesma ordem das promessas transmitidas.

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 alguns segundos mais rápido do que o carregamento individual, e o código fica menor do que a primeira tentativa. É possível fazer o download dos capítulos em qualquer ordem, mas eles aparecem na tela na ordem correta.

No entanto, ainda podemos melhorar o desempenho percebido. Quando o capítulo um chegar, deveremos adicioná-lo à página. Isso permite que o usuário comece a ler antes da chegada dos outros capítulos. Quando o capítulo três chegar, não o adicionaremos à página porque o usuário pode não perceber que o capítulo dois está ausente. Quando o capítulo dois chegar, poderemos adicionar os capítulos dois, três etc.

Para fazer isso, buscamos o JSON para 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 aqui está, o melhor dos dois! O tempo necessário para entregar todo o conteúdo é o mesmo, mas o usuário recebe a primeira parte dele mais cedo.

Neste exemplo trivial, todos os capítulos chegam aproximadamente no mesmo horário, mas o benefício de exibir um de cada vez será ainda maior para capítulos mais grandes.

Fazer isso com callbacks ou eventos no estilo Node.js exige o dobro de código, mas não é tão fácil de acompanhar. No entanto, este não é o fim do assunto para promessas, quando combinadas com outros recursos do ES6, elas ficam ainda mais fáceis.

Rodada bônus: recursos expandidos

Desde que escrevi este artigo, a capacidade de usar promessas aumentou muito. Desde o Chrome 55, as funções assíncronas permitem que códigos baseados em promessas sejam escritos como se fossem síncronos, mas sem bloquear a linha de execução principal. Leia mais sobre isso no my async functions article. Há um suporte generalizado para promessas e funções assíncronas nos principais navegadores. Confira os detalhes na referência de promessa e função assíncrona do MDN.

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

Agradecemos também a Mathias Bynens por atualizar várias partes do artigo.