Promessas no Node.js: uma alternativa aos callbacks

7 min de leitura
Patrocinado
Imagem de: Promessas no Node.js: uma alternativa aos callbacks
Avatar do autor

Equipe TecMundo

Os callbacks são os mecanismos mais simples possíveis para lidar com código assíncrono no JavaScript. No entanto, os callbacks crus sacrificam o controle de fluxo, o tratamento de exceções e a semântica de funções com as quais os desenvolvedores estão familiarizados no código síncrono:

// Asynchronous operations return no meaningful value

var noValue = fs.readFile('file1.txt', function(err, buf) {

  // Errors are explicitly handled every time

  if (err) return handleErr(err)

  fs.readFile('file2.txt', function(err2, buf2) {

    if (err2) return handleErr(err2)

    data.foo.baz = 'bar' // Exceptions like this ReferenceError are not caught

    // Sequential operations encourage heavy nesting

    fs.readFile('file3.txt', function(err3, buf3) {

      if (err3) return handleErr(err3)

    })

  })

})

As promessas oferecem uma maneira de recuperar esse controle com:

  • Controle de fluxo mais poderoso

  • Melhor tratamento de exceções

  • Semântica de programação funcional

Mesmo assim, promessas podem ser um tópico confuso, então você pode tê-las ignorado ou pulado direto para async/await, que adiciona uma nova sintaxe para promessas ao JavaScript.

No entanto, entender como as promessas funcionam e se comportam em um nível fundamental o ajudará a aproveitá-las ao máximo. Neste artigo, abordaremos o básico sobre promessas, incluindo o que são, como criá-las e como usá-las da forma mais eficaz.

Promessas no abstrato

Primeiro, vamos olhar para o comportamento das promessas: o que elas são e como podem ser úteis? Depois, discutiremos sobre como as criar e usar.

O que é uma promessa? Vamos conferir a definição:

A promessa é uma abstração para programação assíncrona. É um objeto que procura o valor de retorno ou pela exceção lançada por uma função que tem que fazer um processamento de forma assíncrona. – Kris Kowal no JavaScript Jabber

O componente principal de um objeto promessa é o método then, que é como você recebe o valor de retorno (conhecido como valor de realização) ou a exceção lançada (conhecida como razão da rejeição) de uma operação assíncrona. O then toma dois callbacks opcionais como argumentos, o que iremos chamar de onFulfilled e onRejected:

let promise = doSomethingAync()

promise.then(onFulfilled, onRejected)

onFulfilled e onRejected são desencadeados quando a promessa se resolve (o processamento assíncrono é completado). Uma dessas funções será desencadeada porque apenas uma resolução é possível.

Dos callbacks às promessas

Tendo esse conhecimento básico das promessas, vamos observar um callback assíncrono familiar no Node.js:

readFile(function(err, data) => {

  if (err) return console.error(err)

  console.log(data)

})

Se nossa função readFile retornasse uma promessa, nós escreveríamos a mesma lógica como:

let promise = readFile()

promise.then(console.log, consoler.error)

A princípio, parece que a estética mudou. Porém, agora temos acesso a um valor representando a operação assíncrona (a promessa). Podemos passar a promessa em código como qualquer outro valor em JavaScript. Qualquer um com acesso à promessa pode consumi-la usando then não importando se a operação assíncrona está completa ou não. Nós também temos garantias de que o resultado da operação assíncrona não mudará de alguma forma, pois a promessa se resolverá uma vez só (seja realizada ou rejeitada).

É útil pensar sobre then não como uma função que leva dois callbacks (onFulfilled e onRejected), mas como uma que desembrulha a promessa para revelar o que aconteceu a partir da operação assíncrona. Qualquer um com acesso à promessa pode usar then para desembrulhá-la. Para saber mais sobre essa ideia, leia Os callbacks são imperativos, as promessas são funcionais: a maior oportunidade perdida no Node.

Promessas encadeadas ou aninhadas

O próprio método then retorna uma promessa:

let promise = readFile()

let promise2 = promise.then(readAnotherFile, console.error)

Essa promessa representa o valor de retorno para seus handlers onFulfilled ou onRejected, se especificado. Já que apenas uma resolução é possível, a promessa procura o handler desencadeado:

let promise = readFile()

let promise2 = promise.then(

  function(data) {

    return readAnotherFile() // If readFile was successful, let's readAnotherFile

  },

  function(err) {

    console.error(err) // If readFile was unsuccessful, let's log it but still readAnotherFile

    return readAnotherFile()

  }

)

promise2.then(console.log, console.error) // The result of readAnotherFile

Uma vez que o then retorna uma promessa, isso significa que as promessas podem se encadear juntas para evitar o aninhamento profundo do callback hell:

readFile()

  .then(readAnotherFile)

  .then(doSomethingElse)

  .then(...)

Mesmo assim, as promessas podem se aninhar quando manter um fechamento é importante:

readFile().then(function(data) {

  return readAnotherFile().then(function() {

    // Do something with `data`

  })

})

Promessas e funções síncronas

As promessas modelam funções síncronas de formas importantes. Uma delas é usando return para continuação em vez de chamar outra função. Os exemplos anteriores retornaram readAnotherFile() para sinalizar o que fazer após readFile().

Se você retorna uma promessa, ela sinalizará o próximo then quando a operação assíncrona estiver completa. Você também pode retornar qualquer outro valor e o próximo onFulfilled pegará o valor como argumento:

readFile()

  .then(function (buf) {

    return JSON.parse(buf.toString())

  })

  .then(function (data) => {

    // Do something with `data`

  })

Lidando com erros nas promessas

Você também pode usar a palavra-chave throw e obter semântica try/catch. Essa deve ser uma das características mais poderosas das promessas. Por exemplo, considere o código síncrono a seguir:

try {

  doThis()

  doThat()

} catch (err) {

  console.error(err)

}

Nesse exemplo, se doThis() ou doThat() lançassem um erro, nós o pegaríamos e registraríamos. Uma vez que blocos try/catch permitem operações agrupadas, podemos evitar ter que lidar com erros explicitamente para cada operação. Nós podemos fazer o mesmo de forma assíncrona com as promessas:

doThisAsync()

  .then(doThatAsync)

  .then(undefined, console.error)

Se doThisAsync() não tiver sucesso, a promessa é rejeitada, e o próximo then com um handler onRejected desencadeia. Nesse caso, é a função console.error. E, assim como os blocos try/catch, doThatAsync() nunca seria chamado.  Essa é uma melhora com relação aos callbacks crus, nos quais você tem que lidar com os erros de forma explícita a cada passo.

Mas ainda melhora! Qualquer exceção lançada — implícita ou explícita — dos callbacks then também é tratada nas promessas:

doThisAsync()

  .then(function(data) {

    data.foo.baz = 'bar' // Throws a ReferenceError as foo is not defined

  })

  .then(undefined, console.error)

Aqui, o ReferenceError levantado desencadeia o próximo handler onRejected na cadeia. Muito bom! E é claro que isso funciona para o explícito throw também:

doThisAsync()

  .then(function(data) {

    if (!data.baz) throw new Error('Expected baz to be there')

  })

  .catch(console.error) // The catch(fn) is shorthand for .then(undefined, fn)

Uma observação importante com tratamento de erros

Como afirmado anteriormente, as promessas imitam a semântica try/catch. Em um bloco try/catch, é possível mascarar um erro ao nunca o tratar explicitamente:

try {

  throw new Error('Never will know this happened')

} catch (e) {}

O mesmo ocorre com as promessas:

readFile().then(function(data) {

  throw new Error('Never will know this happened')

})

Para expor os erros encobertos, a solução é terminar a cadeia da promessa com uma cláusula simples .catch(onRejected):

readFile()

  .then(function(data) {

    throw new Error('Now I know this happened')

  })

  .catch(console.error)

Bibliotecas de terceiros incluem opções para expor rejeições não tratadas.

Promessas de forma concreta

Nosso exemplos utilizaram métodos fictícios que retornam promessas para ilustrar o método then do JavaScprit ES6/2015 e do Promises/A+. Agora vamos observar exemplos mais concretos.

Convertendo callbacks em promessas

Você deve estar se perguntando sobre como criar uma promessa, para começar. A API para criar uma promessa não está especificada no Promise/A+ porque não é necessária para a interoperabilidade. O ES6/2015 padronizou um construtor Promise, ao qual retornaremos em breve. Um dos casos mais comuns para o uso de promessas é quando há a necessidade de converter bibliotecas existentes baseadas em callbacks. Aqui, o Node tem uma função de utilidade embutida, util.promisify, para nos ajudar.

Vamos converter uma das funções centrais assíncronas do Node, que incluem callbacks, para retornar promessas usando, em vez disso, util.promisify:

const util = require('util')

const fs = require('fs')

let readFile = util.promisify(fs.readFile)

let promise = readFile('myfile.txt')

promise.then(console.log, console.error)

Criando promessas cruas

Você pode criar uma promessa usando o construtor Promise também. Vamos converter o mesmo método fs.readFile para retornar promessas sem usar o util.promisify:

const fs = require('fs')

function readFile(file, encoding) {

  return new Promise(function(resolve, reject) {

    fs.readFile(file, encoding, function(err, data) {

      if (err) return reject(err) // Rejects the promise with `err` as the reason

      resolve(data) // Fulfills the promise with `data` as the value

    })

  })

}

let promise = readFile('myfile.txt')

promise.then(console.log, console.error)

Fazendo APIs que suportam tanto os callbacks quanto as promessas

Nós vimos duas formas de transformar código callback em código de promessa. Você também pode fazer APIs que fornecem tanto uma interface de promessa quanto de callback. Por exemplo, vamos transformar fs.readFile em uma API que suporta callbacks e promessas:

const fs = require('fs')

function readFile(file, encoding, callback) {

  if (callback) return fs.readFile(file, encoding, callback) // Use callback if provided

  return new Promise(function(resolve, reject) {

    fs.readFile(file, encoding, function(err, data) {

      if (err) return reject(err)

      resolve(data)

    })

  })

}

Se um callback existir, você pode desencadeá-lo com argumentos padrão no estilo Node (err, result).

readFile('myfile.txt', 'utf8', function(er, data) {

  // ...

})

Fazendo operações paralelas com promessas

Nós falamos sobre operações assíncronas sequenciais. Para operações paralelas, o ES6/2015 fornece o método Promise.all, que absorve um arranjo de promessas e retorna uma nova. A nova promessa se completa depois que todas as operações são concluídas. Se qualquer uma das operações falhar, a nova promessa é rejeitada.

let allPromise = Promise.all([readFile('file1.txt'), readFile('file2.txt')])

allPromise.then(console.log, console.error)

É importante notar novamente que as promessas imitam funções. Uma função possui um valor de retorno. Quando se passa ao Promise.all duas promessas que se completam, o onFulfilled é desencadeado com um argumento (um arranjo com ambos os resultados). Isso pode surpreendê-lo; no entanto, a consistência com duplicados síncronos é uma garantia importante que as promessas fornecem.

Tornando as promessas ainda mais concretas

A melhor forma de entender as promessas é usando-as. Aqui estão algumas ideias para você começar:

  • Embrulhe algumas funções de biblioteca padrão do Node.js, convertendo callbacks em promessas. Não cole usando o node.promisify!

  • Pegue uma função usando async/await e reescreva-a sem usar essa sintaxe. Isso significa que você retornará uma promessa e usará o método then.

  • Escreva algo recursivamente usando promessas (uma árvore de diretório seria um bom começo).

  • Escreva uma implementação Promise A+. Aqui está a minha.

...

Quer ler mais conteúdo especializado de programação? Conheça a IBM Blue Profile e tenha acesso a matérias exclusivas, novas jornadas de conhecimento e testes personalizados. Confira agora mesmo, consiga as badges e dê um upgrade na sua carreira!

.....

Participe da Maratona Behind the Code 2020, um desafio para desenvolvedores e entusiastas da tecnologia! Além de concorrer a prêmios, você ainda tem acesso a conteúdos e serviços gratuitos. Não perca essa chance, as inscrições vão até 7 de agosto!

Você sabia que o TecMundo está no Facebook, Instagram, Telegram, TikTok, Twitter e no Whatsapp? Siga-nos por lá.