Async/await: escrita assíncrona síncrona

1. Fundamentos do Async/await

O JavaScript é single-threaded, mas gerencia operações assíncronas através do event loop. Antes do async/await, lidávamos com callbacks e Promises — ambos funcionais, mas propensos a aninhamentos complexos ou encadeamentos verbosos.

O async transforma qualquer função em uma função que retorna uma Promise implicitamente. O await pausa a execução da função assíncrona até que a Promise seja resolvida, sem bloquear a thread principal. Isso permite escrever código assíncrono com a mesma linearidade do código síncrono.

// Função síncrona
function somar(a, b) {
  return a + b;
}

// Função assíncrona equivalente
async function somarAsync(a, b) {
  return a + b; // automaticamente wrapped em Promise.resolve()
}

somarAsync(2, 3).then(console.log); // 5

A diferença crucial: enquanto o código síncrono bloqueia a execução até obter o resultado, o async/await libera a thread para outras tarefas durante a espera.

2. Sintaxe e Declaração de Funções Assíncronas

Podemos declarar funções assíncronas de várias formas:

// Declaração tradicional
async function buscarUsuario(id) {
  const resposta = await fetch(`https://api.exemplo.com/usuarios/${id}`);
  return resposta.json();
}

// Arrow function assíncrona
const buscarUsuario = async (id) => {
  const resposta = await fetch(`https://api.exemplo.com/usuarios/${id}`);
  return resposta.json();
};

// Método em objeto/class
const api = {
  async getUsuario(id) {
    return await fetch(`/usuarios/${id}`).then(res => res.json());
  }
};

O await só pode ser usado dentro de funções async. Ele espera a resolução de qualquer Promise — seja de fetch, setTimeout convertido em Promise, ou APIs personalizadas.

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

async function exemploDelay() {
  console.log('Início');
  await delay(2000);
  console.log('Após 2 segundos');
}

3. Tratamento de Erros com try/catch

Erros em funções assíncronas são capturados com try/catch, similar ao código síncrono:

async function buscarDados() {
  try {
    const resposta = await fetch('https://api.exemplo.com/dados');
    if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
    const dados = await resposta.json();
    return dados;
  } catch (erro) {
    console.error('Falha na requisição:', erro.message);
    throw erro; // re-lança para quem chamou tratar
  } finally {
    console.log('Operação finalizada');
  }
}

Erros não capturados em funções async propagam como Promises rejeitadas. É essencial ter um catch global ou usar process.on('unhandledRejection') no Node.js.

4. Async/await vs. Promises Tradicionais

Comparação direta entre encadeamento .then() e async/await:

// Com Promises encadeadas
fetch('/usuario/1')
  .then(res => res.json())
  .then(usuario => fetch(`/posts?userId=${usuario.id}`))
  .then(res => res.json())
  .then(posts => console.log(posts))
  .catch(err => console.error(err));

// Com async/await
async function carregarPostsDoUsuario() {
  try {
    const resUsuario = await fetch('/usuario/1');
    const usuario = await resUsuario.json();
    const resPosts = await fetch(`/posts?userId=${usuario.id}`);
    const posts = await resPosts.json();
    console.log(posts);
  } catch (err) {
    console.error(err);
  }
}

Async/await oferece legibilidade superior para fluxos sequenciais. No entanto, Promises ainda são úteis para paralelismo com Promise.all.

5. Execução Paralela com Async/await

Para executar múltiplas operações simultaneamente, combinamos await com Promise.all:

async function carregarDashboard() {
  try {
    const [usuarios, produtos, pedidos] = await Promise.all([
      fetch('/api/usuarios').then(r => r.json()),
      fetch('/api/produtos').then(r => r.json()),
      fetch('/api/pedidos').then(r => r.json())
    ]);
    return { usuarios, produtos, pedidos };
  } catch (erro) {
    console.error('Falha ao carregar dashboard:', erro);
  }
}

Cuidado com loops: forEach não respeita await corretamente. Prefira for...of ou Promise.all com map:

// ERRADO - forEach não espera
async function processarItensErrado(itens) {
  itens.forEach(async (item) => {
    await processar(item); // executa concorrentemente, não em série
  });
}

// CORRETO - for...of para série
async function processarItensSerie(itens) {
  for (const item of itens) {
    await processar(item);
  }
}

// CORRETO - Promise.all para paralelo
async function processarItensParalelo(itens) {
  await Promise.all(itens.map(item => processar(item)));
}

6. Async/await no Node.js

No Node.js, operações de I/O se beneficiam enormemente do async/await:

import { readFile, writeFile } from 'fs/promises';

async function processarArquivo() {
  try {
    const dados = await readFile('./entrada.txt', 'utf-8');
    const processado = dados.toUpperCase();
    await writeFile('./saida.txt', processado);
    console.log('Arquivo processado com sucesso');
  } catch (erro) {
    console.error('Erro no processamento:', erro);
  }
}

Em middlewares Express, funções assíncronas precisam de tratamento explícito de erros:

import express from 'express';
const app = express();

// Wrapper para capturar erros assíncronos
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/usuario/:id', asyncHandler(async (req, res) => {
  const usuario = await buscarUsuario(req.params.id);
  res.json(usuario);
}));

// Ou usar express-async-errors
import 'express-async-errors';
app.get('/usuario/:id', async (req, res) => {
  const usuario = await buscarUsuario(req.params.id);
  res.json(usuario);
});

7. Async/await no React

No React, useEffect não aceita diretamente funções async. Usamos uma função interna ou IIFE:

import { useState, useEffect } from 'react';

function ListaUsuarios() {
  const [usuarios, setUsuarios] = useState([]);
  const [loading, setLoading] = useState(true);
  const [erro, setErro] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    const carregarUsuarios = async () => {
      try {
        setLoading(true);
        const resposta = await fetch('/api/usuarios', {
          signal: controller.signal
        });
        if (!resposta.ok) throw new Error('Erro na requisição');
        const dados = await resposta.json();
        setUsuarios(dados);
      } catch (erro) {
        if (erro.name !== 'AbortError') {
          setErro(erro.message);
        }
      } finally {
        setLoading(false);
      }
    };

    carregarUsuarios();

    // Cleanup para evitar memory leaks
    return () => controller.abort();
  }, []);

  if (loading) return <div>Carregando...</div>;
  if (erro) return <div>Erro: {erro}</div>;

  return (
    <ul>
      {usuarios.map(usuario => (
        <li key={usuario.id}>{usuario.nome}</li>
      ))}
    </ul>
  );
}

O AbortController previne atualizações de estado em componentes desmontados, evitando memory leaks.

8. Boas Práticas e Armadilhas Comuns

Armadilhas frequentes:

// 1. await desnecessário
async function exemplo() {
  const promise = fetch('/api'); // já é Promise
  const resultado = await promise; // OK, mas poderia ser direto
}

// 2. Misturar callbacks com async
async function lerArquivoCallback() {
  // ERRADO: callback não espera await
  fs.readFile('arquivo.txt', async (err, data) => {
    await processar(data); // pode causar erros não capturados
  });
}

// 3. Erro não tratado em Promise.all
async function exemploParalelo() {
  const resultados = await Promise.all([
    operacaoQuePodeFalhar(),
    outraOperacao()
  ]).catch(err => {
    // Se uma falhar, todas falham
    console.error('Uma das operações falhou');
  });
}

Boas práticas:

  • Sempre trate erros com try/catch em funções async
  • Use Promise.allSettled quando precisar de resultados parciais
  • Evite await dentro de loops quando as operações são independentes
  • Prefira for...of para operações sequenciais
  • Utilize AbortController no React para cancelar requisições

Async/await revolucionou a forma como escrevemos código assíncrono em JavaScript, tornando-o mais legível e manutenível. Dominar seus padrões e armadilhas é essencial para qualquer desenvolvedor moderno.

Referências