Programação Assíncrona - Callbacks, Promise, async/await

15 min leitura | 2024.12.23

O que é Processamento Assíncrono

Processamento assíncrono é um mecanismo que permite continuar outras operações sem esperar por operações demoradas (leitura de arquivos, chamadas de API, timers, etc.).

flowchart LR
    subgraph Sync["Processamento Síncrono (espera sequencial)"]
        S1["Tarefa 1"] --> S1C["Concluída"] --> S2["Tarefa 2"] --> S2C["Concluída"] --> S3["Tarefa 3"] --> S3C["Concluída"]
    end
flowchart TB
    subgraph Async["Processamento Assíncrono (execução concorrente)"]
        A1["Tarefa 1 Início"]
        A2["Tarefa 2 Início"]
        A3["Tarefa 3 Início"]
        A1C["Tarefa 1 Concluída"]
        A2C["Tarefa 2 Concluída"]
        A3C["Tarefa 3 Concluída"]
    end

Event Loop

JavaScript é single-threaded, mas realiza processamento assíncrono através do event loop.

flowchart TB
    CallStack["Call Stack<br/>(executa código síncrono)"]
    EventLoop["Event Loop<br/>(retira da fila quando a call stack está vazia)"]
    Microtask["Microtask Queue<br/>(Promise, queueMicrotask)<br/>★ Prioridade: Alta"]
    Macrotask["Macrotask Queue<br/>(setTimeout, I/O)<br/>Prioridade: Baixa"]

    CallStack <--> EventLoop
    EventLoop --> Microtask
    EventLoop --> Macrotask

Exemplo de Ordem de Execução

console.log('1'); // Síncrono

setTimeout(() => console.log('2'), 0); // Macrotask

Promise.resolve().then(() => console.log('3')); // Microtask

console.log('4'); // Síncrono

// Saída: 1, 4, 3, 2

Callback

É o padrão assíncrono mais básico.

// Exemplo de callback hell
fs.readFile('file1.txt', (err, data1) => {
  if (err) throw err;
  fs.readFile('file2.txt', (err, data2) => {
    if (err) throw err;
    fs.readFile('file3.txt', (err, data3) => {
      if (err) throw err;
      console.log(data1, data2, data3);
    });
  });
});

Problemas:

  • Aninhamento profundo (callback hell)
  • Tratamento de erros complexo
  • Baixa legibilidade

Promise

É um objeto que representa a eventual conclusão (ou falha) de uma operação assíncrona.

// Criação de Promise
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('Sucesso!');
    } else {
      reject(new Error('Falhou'));
    }
  }, 1000);
});

// Uso de Promise
myPromise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log('Concluído'));

Promise Chain

fetchUser(userId)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => {
    console.log(comments);
  })
  .catch(error => {
    console.error('Error:', error);
  });

Promise.all / Promise.race

// Promise.all: Espera todos completarem
const results = await Promise.all([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3)
]);
// → [user1, user2, user3]

// Promise.race: Retorna o primeiro a completar
const fastest = await Promise.race([
  fetchFromServer1(),
  fetchFromServer2()
]);

// Promise.allSettled: Obtém todos os resultados (incluindo falhas)
const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(999) // Usuário inexistente
]);
// → [{status: 'fulfilled', value: user1}, {status: 'rejected', reason: Error}]

async/await

É um açúcar sintático para escrever Promises de forma mais intuitiva.

// Função async
async function fetchUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    return { user, posts, comments };
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

Execução Paralela

// Execução sequencial (lenta)
async function sequential() {
  const user1 = await fetchUser(1); // 1 segundo
  const user2 = await fetchUser(2); // 1 segundo
  const user3 = await fetchUser(3); // 1 segundo
  // Total: 3 segundos
}

// Execução paralela (rápida)
async function parallel() {
  const [user1, user2, user3] = await Promise.all([
    fetchUser(1),
    fetchUser(2),
    fetchUser(3)
  ]);
  // Total: ~1 segundo
}

Tratamento de Erros

try/catch

async function fetchData() {
  try {
    const data = await riskyOperation();
    return data;
  } catch (error) {
    if (error.code === 'NOT_FOUND') {
      return null;
    }
    throw error; // Re-throw
  }
}

Wrapper de Erro

// Padrão que retorna erro como array
async function safeAsync(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (error) {
    return [error, null];
  }
}

// Uso
const [error, user] = await safeAsync(fetchUser(id));
if (error) {
  console.error('Failed to fetch user:', error);
  return;
}
console.log(user);

Concorrência e Paralelismo

flowchart LR
    subgraph Concurrent["Concorrência (Concurrent)"]
        direction LR
        Note1["Múltiplas tarefas executam sobrepondo-se no tempo<br/>(realizável mesmo em single-thread)"]
    end

    subgraph Parallel["Paralelismo (Parallel)"]
        direction LR
        Note2["Múltiplas tarefas executam simultaneamente<br/>(requer multi-thread/multi-core)"]
    end

Processamento Paralelo no Node.js

// Worker Threads
const { Worker } = require('worker_threads');

function runCPUIntensiveTask(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./heavy-task.js', {
      workerData: data
    });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

Iteração Assíncrona

// for await...of
async function* generateUsers() {
  for (let id = 1; id <= 3; id++) {
    yield await fetchUser(id);
  }
}

for await (const user of generateUsers()) {
  console.log(user);
}

// AsyncIterator
const stream = fs.createReadStream('large-file.txt');
for await (const chunk of stream) {
  console.log(chunk);
}

Antipadrões

Uso Excessivo de await

// Mau exemplo: await desnecessário
async function bad() {
  return await fetchData(); // await é desnecessário
}

// Bom exemplo
async function good() {
  return fetchData(); // Retorna Promise diretamente
}

Armadilha da Execução Sequencial

// Mau exemplo: Execução sequencial de processos independentes
const user = await fetchUser(id);
const config = await fetchConfig(); // Não depende de user

// Bom exemplo: Execução paralela
const [user, config] = await Promise.all([
  fetchUser(id),
  fetchConfig()
]);

Resumo

A programação assíncrona é essencial para o desenvolvimento moderno de JavaScript. Ela evoluiu de callbacks para Promise e async/await, permitindo escrever código mais legível e manutenível. Ao entender o mecanismo do event loop e realizar tratamento de erros adequado e execução paralela, você pode implementar processamento assíncrono eficiente.

← Voltar para a lista