Promise async/await Javascript

Ainda tem dúvidas sobre callback, Promise e async/await? Este artigo detalha através de exemplos e explicações a utilização destes recursos.

Promise async/await Javascript

Callback

JavaScript é uma linguagem single thread, então cada linha de comando só é executada quando a anterior é finalizada. Execute o comando abaixo no console do seu navegador e veja que o segundo console.log só é executado depois que você clicar no "OK" do alert:

console.log('Olá');
alert('Olha o seu console');
console.log('finalizado');
[Log] Olá
[Log] finalizado
< undefined
Resultado que aparecerá no console

Não se preocupe com a presença desteundefined no console, existe uma explicação mas isto será objeto de estudo em um outro momento.

Mas e se tivermos uma linha de código que demande mais tempo para ser executada? vamos travar toda a tela do usuário? Por sorte a engine do JavaScript nos possibilita ter um comportamento assíncrono e é justamente aí que o callback entra. Então antes de falar sobre Promise vamos falar um pouco sobre callback.

O callback tem o propósito de continuar a execução depois que um processamento assíncrono é efetuado. Isto é extremamente útil, pois podemos executar tarefas em background e deixar configurado para que uma determinada função (callback) seja chamada ao término dessa execução.

Já reparou que o setTimeout trabalha exatamente da forma comentada logo acima? Nós registramos uma função (callback) que é executada após um determinado tempo:

setTimeout(
  () => alert('olá), // callback é executado após 2 segundos
  2000               // 2 segundos
);

Vamos para um outro exemplo, uma requisição AJAX com uma biblioteca bem conhecida, jQuery:

function meuCallback(response) {
  // Quando o $.ajax finalizar
  // a requisição, este método
  // será executado

  // TODO faz alguma coisa
  console.log(response);
}

$.ajax({
  dataType: "json",
  url: url,
  data: data,
  success: meuCallback // << callback
});

Perceba que na chamada do $.ajax passamos uma função (meuCallback) como parâmetro (success). Esta é a função callback que será chamada quando o $.ajax finalizar seu trabalho em background.

Vamos para um outro exemplo bem simples, considere o código abaixo:

function calcular(a, b, callback) {
  setTimeout(
    () => {
      callback(a + b);
    },
    3000 /* 3 segundos */
  );
}

// Podemos declarar a função de callback
// inline...
calcular(1, 2, (resultado) => {
  console.log('O resultado é', resultado);
});

// ... ou podemos criar uma função e depois
// passar seu nome como parâmetro
function processarResultado(resultado) {
  console.log('O resultado é', resultado);
}

calcular(1, 2, processarResultado);

A função calcular recebe 3 parâmetros:

  1. a => primeiro número a ser somado
  2. b => segundo número a ser somado
  3. callback => função que será executada quando calcular finalizar sua execução que ocorre justamente no término do setTimeout, após 3 segundos.

Cole o código acima no console do seu navegador para ver o resultado.

Para finalizar os exemplos, veja abaixo a função readFile do Node.js que também trabalha com callback:

import { readFile } from 'fs';

// 1 (ordem de execução)
readFile('/etc/passwd', /* callback */ (err, data) => {
  // 3
  if (err) {
      // 4 - em caso de erro
      throw err;
  }
  console.log(data);
  // 4
});
// 2

O código acima efetua a leitura de um arquivo e os números cometandos acima representam a sequência de execução do código. Novamente temos um callback, que é o segundo parâmetro da função readFile. Este callback é uma função que recebe dois parâmetros:

  1. err: detalhes do erro, caso ocorra
  2. data: resultado da leitura do arquivo

No exemplo acima veja que o interpretador irá passar pelo readFile iniciando sua execução em background e logo na sequência irá para a próxima linha de comando fora do readFile, que é o ponto marcado com o // 2. Ter essa noção de como será a ordem de execução do código é extremamente importante.

Agora que falamos de callback, vamos falar sobre as Promises.

Promise

Promise é um objeto que representa um valor futuro. Podemos fazer uma analogia á uma compra online, ou seja, após você efetuar a compra você terá um objeto em algum momento no futuro caso tudo ocorra bem, mas algo pode dar errado no meio do caminho e você será informado.

Quando você instancia a Promise, new Promise(), ela está retornando uma "promessa" de que algo será concluído em breve. Para saber quando essa "promessa" será concluída, você deve registrar um callback, ou seja, você "diz" para a Promise qual função deve ser executada quando ela terminar o processamento. A mesma regra vale para erros.

Então uma Promise tem 3 possíveis estados:

  • Pending - estado inicial, pendente de execução
  • fulfilled - concluída com sucesso
  • rejected - ocorreu algum erro

Então temos o seguinte:

const valorFuturo = new Promise(/* ... */);

valorFuturo
  .then(valor => { /* ... */ })   // <-- callback de sucesso
  .catch(erro => { /* ... */ });  // <-- callback de erro

Veja que o .then e o .catch são os métodos que a Promise fornece para registrarmos um callback de sucesso ou erro respectivamente.

Exemplo setTimeout

Tomemos um outro exemplo com o setTimeout:

function consultarDados() {
    return new Promise((resolve) => {
        setTimeout(
            () => resolve('resolvida'),
            2000 /* 2 segundos */
        );
    });
}

consultarDados().then(resultado => {
    console.log(resultado);
});

console.log('olá');
[Log] olá
[Log] resolvida
Mensagens apresentadas no console ao executar o código acima

A função consultarDados retorna uma "promessa" (new Promise()). Para obter o resultado da "promessa" nós registramos um callback através do método then. Então quando consultarDados finalizar sua execução automaticamente a função que passarmos dentro do then será chamada e irá efetuar o console.log do resultado recebido.

O resolve dentro da Promise é o "cara" que fala o seguinte: "terminei meu trabalho, tá aqui meus dados, pode chamar o callback do then()". A mesma lógica vale para erros, porém ao invés de utilizarmos o then vamos pegar os erros no catch e indicar o erro dentro da Promise com o reject:

function consultarDados() {
    return new Promise((resolve, reject) => {
        setTimeout(
            () => reject('ops ocorreu um erro'),
            2000 /* 2 segundos */
        );
    });
}

consultarDados()
    .then(resultado => {
        console.log(resultado);
    }).catch(erro => {
        console.error(erro);
    });

console.log('olá');
[Log] olá
[Error] ops ocorreu um erro
Mensagens apresentadas no console ao executar o código acima

Veja que existe um padrão, sempre que trabalhamos com Promise registramos um callback com o .then para pegar o resultado do processamento em caso de sucesso e o .catch para registrar um callback para pegar os detalhes do erro.

UnhandledPromiseRejectionWarning

Um ponto de atenção: se você registrar um callback apenas no .then vai pegar a seguinte mensagem no console:

UnhandledPromiseRejectionWarning: Unhandled promise rejection (r                                                                                                   ejection id: 1): Error: spawn cmd ENOENT
DeprecationWarning: Unhandled promise rejections are deprecated.
In the future, promise rejections that are not handled will terminate the Node.

Lembre-se de que uma Promise pode ter dois resultados, sucesso ou erro, então sempre devemos tratar no código o .then e o .catch.

Exemplo readFile

Pegando carona no exemplo do readFile do Node.js...

import { readFile } from 'fs';

readFile('/etc/passwd', (err, data) => {
  if (err) {
      throw err;
  }

  console.log(data);
});

...podemos encapsular o readFile em uma função que retorne uma Promise da seguinte forma:

import { readFile } from 'fs';

function lerArquivo() {
  return new Promise((resolve, reject) => {
    readFile('/etc/passwd', (err, data) => {
      if (err) {
          reject(err); // "fala" para o objeto Promise:
                       //   "deu merda, tá aqui os detalhes do erro"
          return;
      }

      resolve(data);  // "fala" para o objeto Promise:
                      //   "acabei, os dados são estes"
    });
  });
}

lerArquivo()
  .then(data => console.log(data))
  .catch(err => console.error(err));

Exemplo fetch

Para finalizar os exemplos vamos utilizar o fetch para efetuar uma requisição AJAX:

const url =
  'https://run.mocky.io/v3/dfd8147a-17ca-4b9c-8887-831c7d99e104';

fetch(url)
  .then(function(response) {
    response.json()
      .then(function(json) {
        console.log(json);
    }).catch(
        errJson => console.error(errJson)
    );
  }).catch(
    err => console.error(err)
  );

Cole o código acima no console do seu navegador e veja o resultado:

Console do navegador mostrando o resultado da execução do código acima
Console do navegador mostrando o resultado da execução do código acima

Repare que fetch() retorna uma Promise, então registramos um callback no .then(). Depois que obtivermos o response chamamos o método .json() que também retorna uma Promise e novamente registramos um outro callback no .then() para obter o resultado final. Observe que temos duas Promise, uma dentro da outra (promise chain). Isso nos leva ao próximo assunto.

Callback hell

Vamos supor que precisamos efetuar 3 operações assíncronas com cada operação dependendo do resultado da execução da operação anterior:

function iniciar() {
  operacao1().then(resultado1 => {
    
    operacao2(resultado1).then(resultado2 => {
        
      operacao3(resultado2).then(resultado3 => {    

        // ****
        // TODO
        // ****
          
      }).catch(erro3 => {
          
      });
        
    }).catch(erro2 => {
      
    });
      
  }).catch(erro1 => {
  
  });
}

Perceba que temos uma função dentro da outra várias vezes. Quando chegamos neste cenário popularmente conhecido como Callback Hell, fica bem difícil de dar manutenção pela péssima legibilidade do código.

Promise - async/await

É justamente no cenário descrito acima que o async/await pode ajudar. Se você estiver utilizando Promise poderá utilizar este recurso. Veja como o código acima pode ser refatorado e depois entramos nos detalhes:

async function iniciar() {
  try {
    const operacao1 = await Operacao1();
    const operacao2 = await Operacao2(operacao1);
    const operacao3 = await Operacao3(operacao2);
  } catch (err) {

  }
}

O await indica que o interpretador do JavaScript deve esperar a conclusão daquela Promise para então seguir para a próxima linha. Veja também que a função iniciar() tem a marcação async indicando que dentro daquela função teremos a utilização do await.

O código ficou muito mais legível e exatamente com a mesma funcionalidade. Vamos tomar novamente o exemplo do fetch citado um pouco mais acima:

(async function() {
    const url =
      'https://run.mocky.io/v3/dfd8147a-17ca-4b9c-8887-831c7d99e104';

    try {
      const response = await fetch(url);
      const json = await response.json();
      console.log(json);
    } catch (err) {
      console.error(err);   
    }
})(); // => o () serve para executar a função - 
      // neste caso é uma função auto-executável

Novamente, o fetch e o response.json() retornam uma Promise, então utilizamos o await em cada uma das chamadas para esperar sua conclusão para então passar para o próximo comando.

A proposta das funções async/await é de simplificar o uso de forma síncrona das Promises e executar alguns procedimentos em um grupo de Promises. Assim como Promises são similares a callbacks estruturados, funções async/awaitsão similares à junção de generators com Promises.

fonte: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Statements/async_function

Considerações

O entedimento claro da Promise e da utilização do async/await pode deixar seu código muito mais legível. Então sempre que se deparar com uma função que retorne uma Promise você poderá considerar a utilização do async/await. Destaco a importância do entedimento do fluxo de execução. Se você ficou com dúvidas, faça alguns testes no seu console para ajudar no entedimento.

Links interessantes: