DEV Community

Cover image for Evitando o Caos Assíncrono: Um Guia Descomplicado sobre Callbacks, Promises e Async/Await!
Matheus Musa
Matheus Musa

Posted on

Evitando o Caos Assíncrono: Um Guia Descomplicado sobre Callbacks, Promises e Async/Await!

Introdução

Fazendo um paralelo com um fascinante conceito da física quântica: a superposição. Na física quântica, uma partícula pode existir em vários estados ao mesmo tempo até que seja observada. Esse fenômeno é conhecido como superposição. De maneira semelhante, em JavaScript, as operações assíncronas permitem que nosso código continue executando várias tarefas simultaneamente, sem esperar que uma operação termine para iniciar a próxima. Imagine que você pediu uma pizza e, enquanto espera, você lê um livro. No mundo da programação, isso é similar ao que fazemos com operações assíncronas: solicitamos algo (como dados de uma API) e, em vez de ficarmos parados aguardando a resposta, continuamos realizando outras tarefas, como ler um livro. Assim como a partícula quântica que não se limita a um único estado até ser medida, nosso programa não fica bloqueado em uma única tarefa, mantendo-se 'em superposição' de atividades.
Esta abordagem não só torna o uso de recursos mais eficiente, mas também reflete a natureza dinâmica e multifacetada dos ambientes de programação modernos. Ao explorar callbacks, promises e async/await, você estará aprendendo a gerenciar essa 'superposição' de tarefas de forma elegante e eficiente, semelhante a um dançarino quântico que se move ao ritmo das probabilidades.

O Que São Callbacks?

Callbacks são funções que são passadas como argumentos para outras funções e são chamadas de volta para sinalizar a conclusão de uma operação assíncrona. Eles são fundamentais em JavaScript para operações como leituras de arquivo, solicitações de rede ou temporizadores.
Exemplo Básico de Callback Um exemplo simples é a leitura de arquivos no Node.js:

const fs = require('fs')
fs.readFile('/caminho/do/arquivo.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Erro ao ler o arquivo:', err);
    return;
  }
  console.log(data);
});
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, fs.readFile é uma função assíncrona que lê um arquivo. O terceiro argumento é um callback que é chamado quando a leitura do arquivo é concluída ou ocorre um erro.

Problemas Comuns com Callbacks:

Callback Hell Um dos maiores problemas com o uso de callbacks é o "Callback Hell" ou "Pyramid of Doom", onde múltiplos callbacks aninhados tornam o código difícil de ler e manter. Por exemplo:

fs.readFile('arquivo1.txt', 'utf8', (err, data1) => {
  if (err) {
    console.error('Erro:', err);
    return
  }
  fs.readFile('arquivo2.txt', 'utf8', (err, data2) => {
    if (err) {
      console.error('Erro:', err);
      return
    }
    // Processamento adicional aqui
  })
})
Enter fullscreen mode Exit fullscreen mode

Este código se torna rapidamente confuso e difícil de gerenciar, especialmente com mais níveis de aninhamento.

Evitando Callback Hell

Uma técnica comum para evitar o Callback Hell é usar funções nomeadas ou modularizar o código em funções separadas. Por exemplo:

function lerArquivo1(callback) {
  fs.readFile('arquivo1.txt', 'utf8', callback);
}
function lerArquivo2(callback) {
  fs.readFile('arquivo2.txt', 'utf8', callback);
}
lerArquivo1((err, data1) => {
  if (err) {
    console.error('Erro:', err)
    return;
  }
  lerArquivo2((err, data2) => {
    if (err) {
      console.error('Erro:', err)
      return;
    }
    // Processamento adicional aqui
  })
})
Enter fullscreen mode Exit fullscreen mode

Este padrão ajuda a manter o código mais organizado e legível.
Comparação com Promises as Promises são uma evolução dos callbacks e oferecem uma maneira mais limpa e menos propensa a erros de lidar com operações assíncronas. Enquanto os callbacks são fundamentais para entender operações assíncronas em JavaScript, as Promises proporcionam uma abordagem mais estruturada e poderosa.

Callbacks

Pontos Fortes:

1. Simplicidade para Tarefas Simples: Para operações assíncronas simples, callbacks podem ser uma solução direta e fácil de entender.
2. Controle Direto: Callbacks oferecem um controle muito direto sobre o fluxo de execução.
**3. Amplamente Suportados: **Callbacks são suportados em todas as versões de JavaScript e em todos os ambientes, sem necessidade de transpilação ou polyfills.

Melhores momentos para usar:

1. Operações Assíncronas Simples: Quando a tarefa assíncrona é direta e não requer encadeamento complexo ou tratamento de erros sofisticado.
2. Compatibilidade Total: **Em ambientes onde a compatibilidade com versões mais antigas do JavaScript é uma preocupação.
**3. Evitando Overhead de Promises:
Em operações críticas de performance onde o overhead adicional de Promises pode ser um fator.

Desvantagens:

1. Callback Hell: Complexidade e dificuldade de manutenção aumentam rapidamente com o aninhamento de callbacks.
2. Dificuldade no Tratamento de Erros: O tratamento de erros em callbacks aninhados pode se tornar confuso e propenso a falhas.
3. Inversão de Controle: O chamador perde o controle do fluxo de execução para a função que recebe o callback, o que pode levar a problemas como a "inversão de controle".

Considerações de Desempenho:

Menos Overhead que Promises: Callbacks têm menos overhead que Promises, o que pode ser benéfico em operações de alta frequência ou em ambientes com recursos limitados.
Risco de Bloqueio de Event Loop: Uso inadequado de callbacks, especialmente em operações síncronas, pode bloquear o loop de eventos e afetar a performance.


O Que São Promises?

Uma Promise no JavaScript é um objeto que representa a eventual conclusão ou falha de uma operação assíncrona. Ela permite que você escreva código que espera por um valor futuro sem bloquear a execução do programa. Uma Promise está em um destes estados:
Pending: Estado inicial, não foi realizada nem rejeitada.
Fulfilled: Operação completada com sucesso.
Rejected: Operação falhou.

Criando uma Promise

Para criar uma Promise, usamos o construtor new Promise() com uma função executora.

let promessa = new Promise((resolve, reject) => {
    // Código assíncrono aqui
    if (/* condição de sucesso */) {
        resolve('Sucesso!')
    } else {
        reject('Erro!')
    }
});
Enter fullscreen mode Exit fullscreen mode

Consumindo uma Promise

Promises são consumidas usando os métodos .then().catch().

promessa
    .then((valor) => console.log(valor)) // 'Sucesso!'
    .catch((erro) => console.log(erro)) // 'Erro!'
Enter fullscreen mode Exit fullscreen mode

Erros Comuns e Dicas

  • Visualização do Fluxo de Promises: Incluir diagramas explicativos pode ser muito útil para entender o fluxo de controle das Promises.
  • Evitando Erros Comuns: Como esquecer de retornar uma nova Promise, não lidar com erros ou criar "Promises penduradas".
  • Comparação com Callbacks: Mostre como as Promises simplificam código em comparação com callbacks, evitando o "Callback Hell".
  • Usando .finally(): Este método é útil para lógica de limpeza, executado independentemente do resultado da Promise.
  • Promises e o Event Loop: Explicação de como as Promises interagem com o Event Loop em JavaScript.
  • Padrões de Projeto: Discussão sobre padrões como "Promise Chaining" e como evitar a "Pyramid of Doom".
  • Tratamento de Exceções: Como lidar com exceções em funções assíncronas e evitar rejeições não tratadas.
  • Debugging de Promises: Técnicas e ferramentas para depurar Promises.
  • Desempenho: O impacto no desempenho ao usar Promises e práticas recomendadas.
  • Atualizações do ES6+: Novidades do ECMAScript que afetam o uso de Promises.
  • Bibliotecas Externas: Como bibliotecas como Bluebird podem expandir a funcionalidade das Promises.

Exemplo Prático: HTTP Requests com Fetch

fetch('https://api.exemplo.com/dados')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('Erro na requisição:', error))
Enter fullscreen mode Exit fullscreen mode

Encadeamento de Promises

let promessa = new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
})
promessa
    .then(valor => valor * 2)
    .then(valor => valor * 2)
    .then(valor => console.log(valor)) // 4
Enter fullscreen mode Exit fullscreen mode

Promise.all

Promise.all([promessa1, promessa2, promessa3])
    .then(valores => console.log(valores))
    .catch(erro => console.error(erro))
Enter fullscreen mode Exit fullscreen mode

Promises

Pontos Fortes:

1. Encadeamento Simples: As Promises permitem um encadeamento claro de operações assíncronas usando .then(), evitando a complexidade dos callbacks aninhados.
2. Tratamento de Erros Centralizado: Com .catch(), as Promises proporcionam um ponto único para capturar erros, tornando o manejo de exceções mais simples e eficaz.
3. Composição e Agregação: Operações como Promise.all() permitem a execução paralela e a agregação de múltiplas Promises, facilitando o trabalho com várias operações assíncronas.

Melhores momentos para usar:

1. Operações Assíncronas Encadeadas: Quando diversas tarefas assíncronas precisam ser executadas em sequência, com cada uma dependendo do resultado da anterior.
2. Controle de Fluxo Complexo: Em cenários onde é necessário um maior controle sobre o fluxo de execução e tratamento de erros em operações assíncronas.
3. Execução Paralela de Tarefas: Quando várias tarefas assíncronas podem ser executadas em paralelo e você precisa aguardar a conclusão de todas elas.

Desvantagens:

1. Curva de Aprendizado: Para quem está começando, o conceito de Promises e seu fluxo de controle pode ser um pouco complexo.
2.Excesso de Encadeamento: Promise chaining pode se tornar complicado em cenários com muitas operações encadeadas.
3. Erros Silenciosos: Se um erro não é capturado corretamente com um .catch(), ele pode passar despercebido.

Considerações de Desempenho:

Memória e Overhead: Cada Promise cria um objeto, o que pode levar a um uso aumentado de memória e overhead, especialmente em grandes quantidades ou em loops intensivos.


Async/Await

Ao se deparar com a programação assíncrona, especialmente no JavaScript, é fácil se sentir sobrecarregado. No entanto, async/await é uma ferramenta que torna essa experiência muito mais gerenciável e compreensível, principalmente para quem está começando. Vamos mergulhar um pouco mais fundo.

Como funciona:

A melhor maneira de pensar no async/await é considerá-lo como uma maneira de fazer o código assíncrono parecer e comportar-se de forma síncrona. Quando você coloca a palavra-chave await antes de uma operação assíncrona, está basicamente dizendo ao JavaScript: "Espere até que isso esteja pronto antes de prosseguir".
O async indica que uma função retornará uma promise. Mesmo que você retorne um valor que não seja uma promise, ele será envolto em uma promise automaticamente.

Buscando dados de uma API

Suponha que você queira buscar dados de uma API externa. A função fetch retorna uma promise, tornando-a perfeita para demonstrar o uso de async/await.

async function buscarDadosAPI(url) {
    try {
        let resposta = await fetch(url)
        let dados = await resposta.json()
        console.log(dados)
    } catch (erro) {
        console.error('Erro ao buscar dados:', erro)
    }
}
buscarDadosAPI('https://api.exemplo.com/items')
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, a resposta da API é esperada e convertida em JSON. Se houver algum erro durante esse processo, ele será capturado pelo bloco catch.

Esperando múltiplas promises

Se tivermos várias operações assíncronas que podem ser executadas em paralelo, podemos usar Promise.all() junto com async/await.

async function obterMúltiplosDados(urls) {
    try {
        let respostas = await Promise.all(urls.map(url => fetch(url)))
        let dados = await Promise.all(respostas.map(resposta => resposta.json()))
        return dados
    } catch (erro) {
        console.error('Erro ao obter dados:', erro)
    }
}
const urls = ['https://api.exemplo.com/item1', 'https://api.exemplo.com/item2']
obterMúltiplosDados(urls).then(dados => console.log(dados))
Enter fullscreen mode Exit fullscreen mode

Simulando um atraso

Podemos usar setTimeout com async/await para simular operações que demoram mais tempo.

function esperar(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
}

async function operacaoDemorada() {
    console.log('Iniciando operação...')
    await esperar(2000); // Espera 2 segundos
    console.log('Operação concluída!')
}
operacaoDemorada()
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, vemos claramente como o async/await torna o código muito mais legível, mesmo que estejamos lidando com uma operação assíncrona.

Trabalhando com armazenamento assíncrono

Muitos ambientes, como navegadores e aplicativos Node.js, oferecem formas de armazenamento assíncrono. Aqui está um exemplo simplificado usando uma abordagem similar:

async function salvarNoBancoDeDados(item) {
    try {
        let resultado = await db.salvar(item)
        console.log('Item salvo com sucesso!', resultado)
    } catch (erro) {
        console.error('Erro ao salvar:', erro)
    }
}
let meuItem = { nome: "Exemplo", valor: 42 }
salvarNoBancoDeDados(meuItem)
Enter fullscreen mode Exit fullscreen mode

Encadeando operações assíncronas

Às vezes, pode ser necessário encadear operações assíncronas, onde o resultado de uma operação é usado na próxima. Isso é facilmente alcançado com async/await.

async function encadeandoOperações(id) {
    try {
        let usuario = await buscarDadosAPI(`https://api.exemplo.com/usuarios/${id}`)
        let posts = await buscarDadosAPI(`https://api.exemplo.com/usuarios/${usuario.id}/posts`)
        console.log(`O usuário ${usuario.nome} tem ${posts.length} posts.`)
    } catch (erro) {
        console.error('Erro:', erro)
    }
}
encadeandoOperações(1)
Enter fullscreen mode Exit fullscreen mode

No exemplo acima, obtemos detalhes de um usuário e, em seguida, usamos o ID desse usuário para buscar seus posts.

Erros Comuns e Dicas

  • Não usar await com funções assíncronas: Um erro comum é esquecer de usar await ao chamar uma função assíncrona. Isso fará com que a função prossiga sem esperar, resultando em comportamentos inesperados.
  • Uso excessivo de async/await: Enquanto async/await torna o código mais limpo, não é necessário para todas as funções assíncronas, especialmente aquelas que não têm operações que requerem espera.
  • Erro ao lidar com exceções: Sempre envolva chamadas await com blocos try/catch para lidar com possíveis erros.

O Conceito de async/await em Ciclos

Uma das características úteis do async/await é sua capacidade de ser usado dentro de loops. Suponha que você queira processar uma série de itens assincronamente:

async function processarItens(itens) {
    for (let item of itens) {
        let resultado = await algumaFuncaoAssincrona(item)
        console.log(resultado)
    }
}
processarItens([1, 2, 3, 4])
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, cada item é processado sequencialmente. No entanto, é crucial notar que isso pode não ser eficiente se a ordem do processamento não importar. Em tais casos, usar Promise.all() pode ser uma escolha melhor.

Pontos Fortes:

1. Legibilidade: O uso de async/await torna o código assíncrono muito mais legível. Não há mais necessidade de encadear .then() ou lidar com callbacks aninhados.
2. Tratamento de Erros: async/await permite usar a estrutura try/catch, que é uma forma familiar de lidar com erros para muitos desenvolvedores.
3. Fluxo de Controle Simplificado: Juntamente com outras construções como loops e condicionais, você pode usar async/await sem se preocupar em encadear ou aninhar funções.

Melhores momentos para usar:

1. Múltiplas Operações Assíncronas Sequenciais: Se você precisa realizar várias tarefas assíncronas uma após a outra e cada tarefa depende do resultado da anterior, async/await torna isso muito mais claro.
2. Combinação de Síncrono e Assíncrono: Se você tem um mix de operações síncronas e assíncronas e quer que elas sejam executadas em uma ordem específica, async/await pode ser uma solução perfeita.
3. Quando o Tratamento de Erros é Crucial: Se a maneira como você lida com os erros é crítica para a sua aplicação, a clareza do try/catch com async/await é inestimável.

Desvantagens:

1. Performance: Em algumas situações, usar async/await pode não ser a opção mais performática, especialmente se as operações assíncronas não dependem uma da outra. Em tais casos, usar Promise.all() pode ser mais eficiente.
2. Complexidade Potencial: Para iniciantes, pode ser um pouco confuso no começo entender que mesmo funções async que não têm await ainda retornam promises.
3. Compatibilidade: Enquanto a maioria dos ambientes modernos suporta async/await, ainda pode haver situações ou plataformas mais antigas onde essa sintaxe não é suportada. Embora existam transpiladores como Babel para contornar isso, ainda é uma consideração a ter em mente.

Considerações de Desempenho ao Usar async/await

Overhead de Runtime:

  • Cada função async adiciona um pequeno overhead de runtime, pois transforma o código em uma máquina de estados por baixo dos panos.
  • Isso pode ser insignificante para aplicativos menores ou operações que não são chamadas frequentemente, mas em loops intensivos ou em operações de alta frequência, pode ser notável.

Consumo de Memória:

  • As funções async podem levar a um maior uso de memória, pois cada chamada de função async retorna uma promise, e cada await pode adicionar um frame ao stack de chamadas, o que pode aumentar o consumo de memória.

Bloqueio de Loop de Eventos:

  • O uso impróprio do await pode levar ao bloqueio do loop de eventos se uma operação síncrona pesada for realizada entre os awaits, o que pode afetar a performance geral da aplicação.

O async/await não é apenas uma característica de sintaxe, mas uma abordagem fundamentalmente nova para lidar com operações assíncronas em JavaScript. Para os iniciantes, ele elimina muitas das confusões associadas às promises e callbacks, apresentando uma forma linear e compreensível de entender o fluxo assíncrono.
No entanto, como em todas as técnicas e recursos, é crucial usá-lo com discernimento. Sempre pense sobre a real necessidade de sequencialidade em suas operações assíncronas e considere se há uma abordagem mais eficiente.

Conclusão Geral: Callbacks, Promises e Async/Await em JavaScript

Callbacks, Promises e Async/Await são ferramentas fundamentais em JavaScript para lidar com operações assíncronas. Cada uma delas oferece diferentes níveis de abstração e facilidades, adequadas para diferentes cenários de programação assíncrona.
Callbacks são a forma mais básica de manipulação assíncrona em JavaScript. Eles são simples, diretos e suportados universalmente, mas podem rapidamente levar ao complexo e difícil de manter "Callback Hell". São mais adequados para operações assíncronas simples e quando a compatibilidade com ambientes mais antigos é uma preocupação.
Promises representam uma evolução em relação aos callbacks, fornecendo uma abstração mais poderosa e flexível. Elas permitem um encadeamento mais claro e um tratamento de erros mais centralizado, facilitando a leitura e a manutenção do código. Promises são ideais para fluxos de controle mais complexos e quando várias operações assíncronas precisam ser coordenadas ou executadas em paralelo.
Async/Await, construído sobre Promises, eleva a legibilidade e a simplicidade do código assíncrono a um novo patamar. Ele permite escrever código assíncrono que se parece e se comporta mais como código síncrono, tornando-o mais fácil de entender e manter. Async/Await é particularmente útil quando se lida com múltiplas operações assíncronas sequenciais e em cenários onde o tratamento de erros e a clareza do fluxo de controle são críticos.

Comparação e Escolha:

  • Complexidade vs. Simplicidade: Callbacks são simples mas podem se tornar complexos em operações aninhadas, enquanto Promises e Async/Await oferecem uma abordagem mais gerenciável para complexidades crescentes.
  • Legibilidade: Async/Await ganha em termos de legibilidade e simplicidade, especialmente para desenvolvedores menos familiarizados com as peculiaridades das operações assíncronas.
  • Performance: Callbacks têm menos overhead em comparação com Promises e Async/Await, o que pode ser um fator decisivo em ambientes de alto desempenho ou com recursos limitados.
  • Compatibilidade: Callbacks são universais, mas Async/Await e algumas funcionalidades de Promises podem exigir transpilação para suporte em navegadores mais antigos.

Conclusão: Ao desenvolver em JavaScript, a escolha entre callbacks, promises e async/await deve ser guiada pelo contexto específico do projeto, a necessidade de legibilidade e manutenção do código, a complexidade das operações assíncronas envolvidas, e as considerações de desempenho e compatibilidade. Entender as forças e limitações de cada abordagem é essencial para escrever código assíncrono eficiente e sustentável em JavaScript.
Para criar uma seção de referências para o seu artigo, podemos listar fontes que abordam os conceitos de callbacks, promises, e async/await em JavaScript, assim como a analogia com a física quântica. Aqui estão algumas sugestões:

Referências

Top comments (0)