Javascript Promises

Trabalhar com uma grande quantidade de JavaScript assíncrono e ao mesmo tempo com o tratamento de erros nunca foi tarefa fácil. É neste momento que entram em cena as Promises, utilizadas justamente para facilitar nossas vidas como desenvolvedores, dando a você uma maneira de organizar seus callbacks sem cair na chamada pirâmide da desgraça dos callbacks.

Uma promessa é um objeto utilizado para processamento de uma operação assíncrona (como uma requisição HTTP ou a leitura de uma arquivo no disco) o qual você adiciona callbacks de falha ou conclusão. Só pode ser concluída ou falhar uma vez e não pode alternar entre conclusão e falha.

Abordagem tradicional

Se desejamos que o navegador carregue uma imagem de maneira assíncrona com base no src e queremos saber se houve sucesso ou falha, poderíamos fazer o seguinte:

loadImage('imagem.png', function (img) {
    // Imagem carregada com sucesso
}, function (e) {
    // Não foi possível carregar a imagem
    console.log(e);
});

function loadImage(url, success, error) {
    var img = new Image();
    img.src = url;
    img.onload = function () {
        success(img);
    };
    img.onerror = function (e) {
        error(e);
    };
}

Neste caso estamos passando dois callbacks como argumentos para a função loadImage. Como o loadImage é assíncrono, ele aceita retornos de chamada em vez de retorno imediato.

Abordagem com Promessa

Agora vamos alterar a função loadImage para retornar um objeto Promise, onde iremos anexar os retornos de chamada para a promessa em vez de passá-los como argumentos para a função. Uma possível implementação seria:

function loadImage(url) {
    return new Promise(function (resolve, reject) {
        var img = new Image();
        img.src = url;
        img.onload = function () {
            resolve(img);
        };
        img.onerror = function (e) {
            reject(e);
        };
    });
}

Repare que Promises recebem duas funções por parâmetro, resolve e reject, que serão executadas após a conclusão de um trabalho assíncrono. A primeira serve para resolver a promise em caso de sucesso e a segunda para rejeitá-la em caso de erro.

A fim de passarmos esses parâmetros, utilizamos as funções then e catch.

  • then: possui dois argumentos, ambos funções callback, sendo uma para o sucesso e outra para o fracasso da promessa;
  • catch: trata apenas casos rejeitados. É o mesmo que then(undefined, failureCallback).

Encadeando

Agora podemos simplesmente encadear as chamadas, numa operação conhecida como composição.

loadImage('imagem.png').then(function (img) {
    // Imagem carregada com sucesso
}).catch(function (e) {
    // Não foi possível carregar a imagem
    console.log(e);
});

Através do encadeamento é possível executar mais ações assíncronas em sequência, como neste exemplo:

func1().then(function(result1) {
    return func2(result1);
  })
  .then(function(result2) {
    return func3(result2);
  })
  .then(function(finalResult) {
    console.log(finalResult);
  })
  .catch(failureCallback);

Cada chamada retorna uma nova promessa que você pode usar para anexar outro retorno de chamada. Mesmo se houver falha ainda podemos continuar realizando novas ações:

func1().then(function (result1) {
    return func2(result1);
})
.then(function (result2) {
    return func3(result2);
})
.then(function (finalResult) {
    console.log(finalResult);
})
.catch(() => {
    console.log('Faça isso em caso de falha');
})
.then(() => {
    console.log('Se ocorrer a falha novamente, faça isso');
});

Estados

Uma promessa pode estar em um destes estados, mutuamente exclusivos:

  • pending (pendente): estado inicial, nenhuma ação foi realizada ou rejeitada;
  • fulfilled (realizada): a ação relacionada à promessa foi concluída;
  • rejected (rejeitado):  ocorreu uma falha na ação;
  • settled (estabelecida): a ação foi concluída ou rejeitada.

O método then é chamado nos estados fulfilled e rejected, porém é importante ficar atento as definições de sucesso e erro, pois uma ação pode ser completada mesmo havendo um erro ou pode não se completar, pois foi cancelada e nenhum erro ocorreu.

Resolvendo ou rejeitando uma promessa

Resolver uma promessa não é o mesmo que cumpri-la. Quando o argumento passado para o resolver é um valor, a promessa é imediatamente cumprida. No entanto, quando outra promessa é passada, as promessas são unidas. Se a promessa aprovada for resolvida, ambas as promessas serão cumpridas, mas se a promessa passada for rejeitada, então ambas as promessas serão rejeitadas. Em resumo, o argumento passado para o resolver dita o destino da promessa. Veja na imagem abaixo:

Depois que uma promessa é realizada ou rejeitada, seu estado e valor nunca podem ser alterados.

Conclusão

A importância das promessas está no fato de nos oferecerem uma alternativa mais simples para executar, compor e gerenciar operações assíncronas quando comparadas a abordagens tradicionais baseadas em retorno de chamada. E apesar de não se tratar de uma novidade, é sempre bom estar atento a maneiras mais elegantes de gerenciar nosso código, não é mesmo?

Happy Coding e até o próximo post!

Compartilhe!