Callbacks: o começo de tudo

[99] Callbacks: o começo de tudo

Antes das Promises, esperar era passar uma função para ser chamada depois. O artigo apresenta o callback assíncrono, a convenção do Node em que o erro vem sempre no primeiro argumento, a pirâmide que nasce quando uma operação depende da anterior, e os casos em que o callback continua sendo a escolha certa.
Javascript

20 min de leitura

No artigo O que é Programação Assíncrona? O Event Loop Explicado entendemos o Event Loop — o mecanismo que permite ao JavaScript ser assíncrono mesmo sendo single-threaded. Agora vamos entender o primeiro padrão que surgiu para lidar com essa assincronicidade: os callbacks.

Callbacks não são exclusivos do código assíncrono — você já os usou bastante nos módulos anteriores. Mas é no contexto assíncrono que eles revelam tanto seu poder quanto seus problemas.

O que é um callback?

Um callback é simplesmente uma função passada como argumento para outra função, que será executada em algum momento — imediatamente ou depois.

// Callback síncrono — executado imediatamente
const numeros = [3, 1, 4, 1, 5, 9];

numeros.forEach(function(numero) {
  console.log(numero); // esta função é um callback
});

// Com arrow function — mais comum
numeros.forEach(numero => console.log(numero));

// filter também usa callback
const pares = numeros.filter(n => n % 2 === 0);

Você já usou callbacks o tempo todo — forEach, map, filter, reduce, addEventListener — todos recebem callbacks. A diferença agora é que vamos usar callbacks para operações assíncronas.

Callbacks assíncronos

Um callback assíncrono é executado depois — quando uma operação demorada termina:

console.log("Antes");

// O callback só executa após 2 segundos
setTimeout(function() {
  console.log("Dentro do callback");
}, 2000);

console.log("Depois");

// Saída:
// Antes
// Depois
// (2 segundos depois)
// Dentro do callback

O JavaScript não espera — ele registra o callback e continua. Quando o tempo passa, o Event Loop coloca o callback na fila para executar.

Simulando operações assíncronas

Em exemplos didáticos, usamos setTimeout para simular operações que levam tempo — como buscar dados de um servidor:

function buscarUsuario(id, callback) {
  console.log(`Buscando usuário ${id}...`);

  // Simula 1.5 segundos de espera (como uma requisição real)
  setTimeout(function() {
    const usuario = {
      id,
      nome: "Ana Paula",
      email: "ana@email.com",
      plano: "premium",
    };
    callback(usuario); // chama o callback com o resultado
  }, 1500);
}

// Usando a função — passamos o que fazer com o resultado
buscarUsuario(42, function(usuario) {
  console.log(`Usuário encontrado: ${usuario.nome}`);
  console.log(`Plano: ${usuario.plano}`);
});

console.log("Código continua executando...");

// Saída:
// Buscando usuário 42...
// Código continua executando...
// (1.5 segundos depois)
// Usuário encontrado: Ana Paula
// Plano: premium

O padrão error-first callback

O Node.js popularizou uma convenção importante para callbacks assíncronos: o primeiro parâmetro é sempre o erro, e o segundo é o resultado. Isso garante consistência no tratamento de falhas:

function buscarProduto(id, callback) {
  setTimeout(function() {
    if (id <= 0) {
      // Primeiro argumento: o erro
      callback(new Error("ID inválido — deve ser maior que zero."), null);
      return;
    }

    if (id > 100) {
      callback(new Error(`Produto ${id} não encontrado.`), null);
      return;
    }

    // Segundo argumento: o resultado
    callback(null, {
      id,
      nome: "Notebook Pro",
      preco: 3500,
      estoque: 15,
    });
  }, 1000);
}

// Usando — sempre verifique o erro primeiro
buscarProduto(42, function(erro, produto) {
  if (erro) {
    console.error(`Erro: ${erro.message}`);
    return; // para aqui se houver erro
  }

  console.log(`Produto: ${produto.nome}`);
  console.log(`Preço: R$ ${produto.preco}`);
});

buscarProduto(-5, function(erro, produto) {
  if (erro) {
    console.error(`Erro: ${erro.message}`); // Erro: ID inválido
    return;
  }
  console.log(produto.nome);
});

O padrão é sempre: callback(erro, resultado). Se deu certo, erro é null. Se deu errado, resultado é null.

Callbacks aninhados — operações dependentes

O problema real começa quando uma operação depende do resultado de outra:

function buscarUsuario(id, cb) {
  setTimeout(() => {
    cb(null, { id, nome: "Carlos", enderecoId: 7 });
  }, 800);
}

function buscarEndereco(enderecoId, cb) {
  setTimeout(() => {
    cb(null, { id: enderecoId, rua: "Av. Brasil", cidade: "São Paulo" });
  }, 600);
}

function buscarPedidos(usuarioId, cb) {
  setTimeout(() => {
    cb(null, [
      { id: 101, total: 250 },
      { id: 102, total: 180 },
    ]);
  }, 700);
}

function calcularFrete(cidade, cb) {
  setTimeout(() => {
    cb(null, { cidade, valor: 25.90, prazo: "3 dias úteis" });
  }, 500);
}

// Para fazer tudo isso em sequência — chegamos ao Callback Hell
buscarUsuario(1, function(erro, usuario) {
  if (erro) return console.error(erro);

  buscarEndereco(usuario.enderecoId, function(erro, endereco) {
    if (erro) return console.error(erro);

    buscarPedidos(usuario.id, function(erro, pedidos) {
      if (erro) return console.error(erro);

      calcularFrete(endereco.cidade, function(erro, frete) {
        if (erro) return console.error(erro);

        // Finalmente chegamos ao resultado
        console.log(`Usuário: ${usuario.nome}`);
        console.log(`Cidade: ${endereco.cidade}`);
        console.log(`Pedidos: ${pedidos.length}`);
        console.log(`Frete: R$ ${frete.valor} — ${frete.prazo}`);

        // E se precisarmos de mais um passo? Mais um nível...
      });
    });
  });
});

Isso é o Callback Hell — também chamado de "pyramid of doom" pela forma que o código toma. Os problemas são claros:

  • Difícil de ler — cresce para a direita indefinidamente
  • Difícil de manter — alterar a ordem é trabalhoso
  • Difícil de tratar erros — cada nível precisa verificar o erro
  • Impossível de reusar — lógica toda acoplada

Amenizando o Callback Hell — funções nomeadas

Uma solução parcial é extrair os callbacks em funções nomeadas:

// Em vez de aninhar tudo, quebramos em funções nomeadas

function aoReceberUsuario(erro, usuario) {
  if (erro) return tratarErro(erro);
  buscarEndereco(usuario.enderecoId, aoReceberEndereco.bind(null, usuario));
}

function aoReceberEndereco(usuario, erro, endereco) {
  if (erro) return tratarErro(erro);
  buscarPedidos(usuario.id, aoReceberPedidos.bind(null, usuario, endereco));
}

function aoReceberPedidos(usuario, endereco, erro, pedidos) {
  if (erro) return tratarErro(erro);
  calcularFrete(endereco.cidade, aoReceberFrete.bind(null, usuario, endereco, pedidos));
}

function aoReceberFrete(usuario, endereco, pedidos, erro, frete) {
  if (erro) return tratarErro(erro);
  console.log(`Usuário: ${usuario.nome}`);
  console.log(`Cidade: ${endereco.cidade}`);
  console.log(`Pedidos: ${pedidos.length}`);
  console.log(`Frete: R$ ${frete.valor}`);
}

function tratarErro(erro) {
  console.error(`Erro: ${erro.message}`);
}

// Inicia a cadeia
buscarUsuario(1, aoReceberUsuario);

Melhor — pelo menos o código não cresce para a direita. Mas ainda é complicado gerenciar o estado entre os níveis e o fluxo não é linear e legível.

Callbacks em paralelo

Às vezes as operações não dependem umas das outras — podem executar simultaneamente. O desafio é saber quando todas terminaram:

function buscarDadosParalelo(ids, callback) {
  const resultados = [];
  let concluidos = 0;
  let houveErro = false;

  ids.forEach((id, index) => {
    buscarProduto(id, function(erro, produto) {
      if (houveErro) return; // já deu errado em outro

      if (erro) {
        houveErro = true;
        return callback(erro, null);
      }

      resultados[index] = produto; // mantém a ordem
      concluidos++;

      if (concluidos === ids.length) {
        callback(null, resultados); // todos concluíram!
      }
    });
  });
}

buscarDadosParalelo([1, 5, 12, 30], function(erro, produtos) {
  if (erro) return console.error(erro);
  console.log(`${produtos.length} produtos carregados.`);
  produtos.forEach(p => console.log(`- ${p.nome}: R$ ${p.preco}`));
});

Isso funciona, mas é verboso e propenso a bugs. As Promises resolvem isso com muito mais elegância — como veremos no próximo artigo.

Casos onde callbacks ainda são a escolha certa

Apesar dos problemas, callbacks são a solução ideal em vários cenários:

// 1. Eventos do DOM — chamados várias vezes
botao.addEventListener("click", (e) => {
  console.log("Clicado!");
});

// 2. Métodos de array — síncronos e funcionais
const dobrados = [1, 2, 3].map(n => n * 2);
const pares = [1, 2, 3, 4].filter(n => n % 2 === 0);

// 3. Operações simples com setTimeout
setTimeout(() => limparMensagem(), 3000);

// 4. APIs que retornam múltiplos eventos ao longo do tempo
// (Streams, WebSockets — Promises só resolvem uma vez)
stream.on("data", (chunk) => processar(chunk));
stream.on("end", () => finalizar());
stream.on("error", (err) => tratarErro(err));

Para operações únicas que levam tempo e podem falhar, as Promises são superiores. Para eventos recorrentes, callbacks são a solução natural.

Exemplo completo — sistema de notificações com callbacks

// Sistema que demonstra callbacks de forma organizada

const sistemaBD = {
  usuarios: [
    { id: 1, nome: "Ana", email: "ana@email.com", notificacoesAtivas: true },
    { id: 2, nome: "Bruno", email: "bruno@email.com", notificacoesAtivas: false },
    { id: 3, nome: "Clara", email: "clara@email.com", notificacoesAtivas: true },
  ],

  buscarUsuario(id, cb) {
    setTimeout(() => {
      const usuario = this.usuarios.find(u => u.id === id);
      if (!usuario) return cb(new Error(`Usuário ${id} não encontrado.`));
      cb(null, usuario);
    }, 500);
  },

  buscarTodos(cb) {
    setTimeout(() => {
      cb(null, [...this.usuarios]);
    }, 600);
  },
};

const emailService = {
  enviar(destinatario, mensagem, cb) {
    setTimeout(() => {
      if (!destinatario.includes("@")) {
        return cb(new Error(`E-mail inválido: ${destinatario}`));
      }
      console.log(`📧 E-mail enviado para ${destinatario}: "${mensagem}"`);
      cb(null, { enviado: true, destinatario });
    }, 300);
  },
};

// Notificar um único usuário
function notificarUsuario(id, mensagem, cb) {
  sistemaBD.buscarUsuario(id, function(erro, usuario) {
    if (erro) return cb(erro);

    if (!usuario.notificacoesAtivas) {
      return cb(null, { enviado: false, motivo: "Notificações desativadas." });
    }

    emailService.enviar(usuario.email, mensagem, function(erro, resultado) {
      if (erro) return cb(erro);
      cb(null, { ...resultado, usuario: usuario.nome });
    });
  });
}

// Notificar todos os usuários com notificações ativas
function notificarTodos(mensagem, cb) {
  sistemaBD.buscarTodos(function(erro, usuarios) {
    if (erro) return cb(erro);

    const ativos = usuarios.filter(u => u.notificacoesAtivas);
    const resultados = [];
    let concluidos = 0;

    if (ativos.length === 0) return cb(null, []);

    ativos.forEach(usuario => {
      emailService.enviar(usuario.email, mensagem, function(erro, resultado) {
        concluidos++;

        if (erro) {
          resultados.push({ usuario: usuario.nome, erro: erro.message });
        } else {
          resultados.push({ usuario: usuario.nome, ...resultado });
        }

        if (concluidos === ativos.length) {
          cb(null, resultados);
        }
      });
    });
  });
}

// Testando
console.log("Iniciando sistema de notificações...\n");

notificarUsuario(1, "Seu pedido foi aprovado!", function(erro, resultado) {
  if (erro) return console.error(`Erro: ${erro.message}`);
  console.log(`Resultado para usuário 1:`, resultado);
});

notificarUsuario(2, "Promoção especial para você!", function(erro, resultado) {
  if (erro) return console.error(`Erro: ${erro.message}`);
  console.log(`Resultado para usuário 2:`, resultado);
});

notificarTodos("Manutenção programada para domingo.", function(erro, resultados) {
  if (erro) return console.error(`Erro geral: ${erro.message}`);
  console.log("\nResultados do envio em massa:");
  resultados.forEach(r => console.log(` - ${r.usuario}: ${r.enviado ? "✓" : "✗"}`));
});

Boas práticas com callbacks

// ✅ 1. Sempre siga o padrão error-first
function operacao(params, callback) {
  // callback(erro, resultado)
  callback(null, resultado); // sucesso
  callback(new Error("msg"), null); // falha
}

// ✅ 2. Sempre verifique o erro primeiro
operacao(params, function(erro, resultado) {
  if (erro) {
    console.error(erro);
    return; // pare aqui
  }
  // use resultado
});

// ✅ 3. Nunca chame o callback duas vezes
//    A guarda só faz sentido quando há mais de um caminho capaz de
//    responder — aqui, o dado e o limite de tempo disputam.
function operacaoSegura(cb) {
  let respondeu = false;

  const responder = (erro, dado) => {
    if (respondeu) return; // a segunda resposta é descartada
    respondeu = true;
    cb(erro, dado);
  };

  buscarDados(responder);
  setTimeout(() => responder(new Error("tempo esgotado")), 5000);

  // Atenção: isto para de OUVIR a operação, não a cancela —
  // ela continua correndo. Cancelar de verdade exige AbortController.
}

// ✅ 4. Extraia callbacks em funções nomeadas
//    para evitar o Callback Hell
function aoReceberDados(erro, dados) { /* ... */ }
operacao(params, aoReceberDados);

// ✅ 5. Para múltiplas operações assíncronas,
//    prefira Promises ou async/await (próximos artigos)

Tarefa para você

Implemente um sistema de carrinho de compras assíncrono usando apenas callbacks:

// Funções disponíveis (implemente com setTimeout):
// buscarProduto(id, callback)  → retorna produto ou erro
// verificarEstoque(id, quantidade, callback) → retorna true/false ou erro
// aplicarCupom(codigo, subtotal, callback) → retorna valor com desconto ou erro
// finalizarPedido(itens, total, callback) → retorna pedido ou erro

// O fluxo deve ser:
// 1. Buscar 2 produtos (em paralelo)
// 2. Verificar estoque de ambos (em paralelo)
// 3. Calcular subtotal
// 4. Aplicar cupom (se fornecido)
// 5. Finalizar pedido
// 6. Exibir resumo ou erro em cada etapa

// Dica: use o padrão de paralelo que vimos para os passos 1 e 2
Ver solução — o carrinho inteiro só com callbacks — inclusive os dois passos em paralelo
// Regra do exercício: nada de Promise, nada de async/await. Só o padrão
// callback(erro, dado) — o mesmo que o Node usou por anos.

const CATALOGO = {
  1: { id: 1, nome: "Caneca", preco: 39.9 },
  2: { id: 2, nome: "Camiseta", preco: 79.9 },
  3: { id: 3, nome: "Adesivo", preco: 5.0 },
};

const ESTOQUE = { 1: 10, 2: 3, 3: 0 };
const CUPONS = { PRIMEIRA10: 0.1, BLACK30: 0.3 };

const latencia = () => 100 + Math.random() * 200;

// ---------------------------------------------------------------
// As quatro funções — sempre (erro, dado), sempre nessa ordem
// ---------------------------------------------------------------
function buscarProduto(id, callback) {
  setTimeout(() => {
    const produto = CATALOGO[id];

    if (!produto) {
      // O erro vai no PRIMEIRO argumento. É convenção, não capricho:
      // é o que permite `if (erro)` como primeira linha de todo
      // callback, sem precisar saber de que função ele veio.
      callback(new Error(`Produto ${id} não existe`));
      return;
    }

    callback(null, produto);
  }, latencia());
}

function verificarEstoque(id, quantidade, callback) {
  setTimeout(() => {
    const disponivel = ESTOQUE[id] ?? 0;

    if (disponivel < quantidade) {
      callback(new Error(
        `Estoque insuficiente do produto ${id}: pedidas ${quantidade}, há ${disponivel}`
      ));
      return;
    }

    callback(null, true);
  }, latencia());
}

function aplicarCupom(codigo, subtotal, callback) {
  setTimeout(() => {
    if (!codigo) {
      callback(null, { total: subtotal, desconto: 0 }); // sem cupom não é erro
      return;
    }

    const percentual = CUPONS[codigo];

    if (percentual === undefined) {
      callback(new Error(`Cupom inválido: ${codigo}`));
      return;
    }

    const desconto = subtotal * percentual;
    callback(null, { total: subtotal - desconto, desconto });
  }, latencia());
}

function finalizarPedido(itens, total, callback) {
  setTimeout(() => {
    if (total <= 0) {
      callback(new Error("Total inválido para finalizar"));
      return;
    }

    callback(null, {
      numero: `PED-${Date.now().toString().slice(-6)}`,
      itens,
      total,
      criadoEm: new Date().toLocaleString("pt-BR"),
    });
  }, latencia());
}

// ---------------------------------------------------------------
// O padrão paralelo, escrito uma vez e reaproveitado
// ---------------------------------------------------------------
// Sem Promise.all, o paralelismo é feito à mão: dispara tudo de uma
// vez, conta quantos voltaram e só chama o callback final no último.
function emParalelo(tarefas, callback) {
  const resultados = new Array(tarefas.length);
  let pendentes = tarefas.length;
  let jaFalhou = false;

  if (pendentes === 0) {
    callback(null, []);
    return;
  }

  tarefas.forEach((tarefa, indice) => {
    tarefa((erro, valor) => {
      // A guarda existe porque as tarefas continuam correndo depois da
      // primeira falha. Sem ela, o callback final seria chamado duas
      // vezes — e chamar callback duas vezes é o bug mais difícil de
      // achar neste estilo de código.
      if (jaFalhou) return;

      if (erro) {
        jaFalhou = true;
        callback(erro);
        return;
      }

      // A posição preserva a ordem original: quem termina primeiro não
      // "passa na frente" no array de resultados.
      resultados[indice] = valor;
      pendentes--;

      if (pendentes === 0) callback(null, resultados);
    });
  });
}

// ---------------------------------------------------------------
// O fluxo dos seis passos
// ---------------------------------------------------------------
function comprar(pedido, callback) {
  const { itens, cupom } = pedido;

  // 1. os dois produtos, em paralelo
  emParalelo(
    itens.map((item) => (cb) => buscarProduto(item.id, cb)),
    (erro, produtos) => {
      if (erro) return callback(erro);

      // 2. estoque dos dois, também em paralelo
      emParalelo(
        itens.map((item) => (cb) => verificarEstoque(item.id, item.quantidade, cb)),
        (erro) => {
          if (erro) return callback(erro);

          // 3. subtotal
          const subtotal = produtos.reduce(
            (soma, produto, i) => soma + produto.preco * itens[i].quantidade,
            0
          );

          // 4. cupom
          aplicarCupom(cupom, subtotal, (erro, { total, desconto }) => {
            if (erro) return callback(erro);

            // 5. fecha
            const linhas = produtos.map((produto, i) => ({
              ...produto,
              quantidade: itens[i].quantidade,
            }));

            finalizarPedido(linhas, total, (erro, pedidoCriado) => {
              if (erro) return callback(erro);

              callback(null, { ...pedidoCriado, subtotal, desconto });
            });
          });
        }
      );
    }
  );
}

// ---------------------------------------------------------------
// 6 — exibindo
// ---------------------------------------------------------------
const reais = (n) => n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });

comprar(
  { itens: [{ id: 1, quantidade: 2 }, { id: 2, quantidade: 1 }], cupom: "PRIMEIRA10" },
  (erro, pedido) => {
    if (erro) {
      console.error(`❌ ${erro.message}`);
      return;
    }

    console.log(`✅ Pedido ${pedido.numero} — ${pedido.criadoEm}`);
    pedido.itens.forEach((i) =>
      console.log(`   ${i.quantidade}x ${i.nome} — ${reais(i.preco * i.quantidade)}`)
    );
    console.log(`   Subtotal: ${reais(pedido.subtotal)}`);
    console.log(`   Desconto: -${reais(pedido.desconto)}`);
    console.log(`   Total:    ${reais(pedido.total)}`);
  }
);

// Um caminho de erro, para ver a mensagem certa aparecer:
comprar({ itens: [{ id: 3, quantidade: 1 }], cupom: null }, (erro) => {
  if (erro) console.error(`❌ ${erro.message}`); // Estoque insuficiente do produto 3
});

// ---------------------------------------------------------------
// O detalhe que morde: `return callback(erro)`
// ---------------------------------------------------------------
// O `return` na frente de cada chamada de erro não devolve valor a
// ninguém — serve só para PARAR a função ali. Sem ele, o código segue
// e chama o callback de novo mais adiante, com sucesso, depois de já
// ter reportado falha. O consumidor recebe as duas coisas e acredita
// na última.
//
// É a diferença entre isto:
//
//   if (erro) callback(erro);        // ❌ segue executando
//   const total = calcular(dados);   // dados é undefined aqui
//
// e isto:
//
//   if (erro) return callback(erro); // ✅ para de verdade

Duas regras salvam o estilo callback: o erro é sempre o primeiro argumento, e toda chamada de erro vem com return na frente. Sem a segunda, a função continua e chama o callback duas vezes — falha e sucesso para a mesma operação, com o consumidor acreditando no que chegou por último.

Callback não é conceito novo deste módulo: toda vez que você passou uma função para map, filter ou addEventListener, era disso que se tratava. O que a assincronia muda é o momento da chamada — e é essa mudança que torna o return inútil, que obriga ao padrão erro-primeiro do Node e que faz o código crescer para a direita quando uma operação depende do resultado da anterior.

Fontes e Referências

Exercícios

Exercício 1

O consumidor está escrito corretamente, com return depois de tratar o erro. Mesmo assim, o que ele imprime?

function buscar(id, cb) {
  setTimeout(() => {
    if (id <= 0) cb(new Error("id inválido"), null);
    cb(null, { id, nome: "Produto" });
  }, 100);
}

buscar(-1, (erro, produto) => {
  if (erro) {
    console.error("erro:", erro.message);
    return;
  }
  console.log("ok:", produto.nome);
});
Ver resposta

✓ Resposta: Imprime erro: id inválido e, logo em seguida, ok: Produto. O consumidor recebe as duas coisas para a mesma operação: falhou e deu certo. O return dele funciona, mas encerra apenas aquela invocação do callback — não impede que o produtor chame o callback de novo, porque o if lá dentro não tem return e a execução segue para a linha de sucesso. É por isso que a convenção do estilo callback é escrever sempre return cb(erro), ou pôr o return na linha seguinte: o return não devolve valor a ninguém, ele existe só para parar a função ali. O sintoma na aplicação é cruel porque o segundo resultado sobrescreve o primeiro na tela: o usuário vê a mensagem de erro piscar e sumir, e o log fica com as duas linhas, sem que nenhum dos dois estados esteja errado isoladamente.

Exercício 2

Alguém trocou resultados[index] = produto por resultados.push(produto). Cada requisição leva um tempo diferente. O que quebra?

function buscarTudo(ids, callback) {
  const resultados = [];
  let concluidos = 0;

  ids.forEach((id, index) => {
    buscarProduto(id, (erro, produto) => {
      if (erro) return callback(erro);

      resultados.push(produto);
      concluidos++;

      if (concluidos === ids.length) callback(null, resultados);
    });
  });
}
Ver resposta

✓ Resposta: A correspondência entre ids e resultados. Com push, os produtos entram na ordem em que as respostas chegam, que é a ordem das latências, não a ordem em que foram pedidos — resultados[0] deixa de ser o produto de ids[0]. Qualquer código que depois cruze as duas listas por posição, como calcular o subtotal multiplicando pelo quantidade do item correspondente, passa a somar preço de um produto com quantidade de outro. E o defeito é do pior tipo: em desenvolvimento, com servidor local e latência quase igual para todos, as respostas voltam quase sempre na ordem certa e tudo parece funcionar; em produção ele aparece de forma intermitente. A atribuição por índice resolve porque cada resposta escreve na sua própria vaga, reservada desde o começo. Vale reparar de passagem numa fragilidade que continua nas duas versões: quem libera o callback final é o contador, então basta um caminho que esqueça de incrementar concluidos para a operação inteira ficar pendurada para sempre — sem erro, sem retorno e sem tempo limite.

Exercício 3

A busca demora 6 segundos e o limite é de 5. Com a guarda respondeu, o que o consumidor recebe? E sem ela?

function operacaoSegura(cb) {
  let respondeu = false;

  const responder = (erro, dado) => {
    if (respondeu) return;
    respondeu = true;
    cb(erro, dado);
  };

  buscarDados(responder);                                        // volta aos 6s
  setTimeout(() => responder(new Error("tempo esgotado")), 5000); // dispara aos 5s
}
Ver resposta

✓ Resposta: Com a guarda, o consumidor recebe o erro de tempo esgotado, aos 5 segundos; o dado que chega aos 6 é silenciosamente descartado. Sem a guarda, ele recebe o erro aos 5 segundos e o sucesso aos 6 — ou seja, a interface já mostrou a tela de falha e um segundo depois começa a desenhar dados por cima dela, um comportamento que costuma ser reportado como "fantasma" porque não se reproduz quando a rede está boa. Há um ponto que a guarda não resolve e que vale entender: o limite de tempo interrompe a escuta, não a operação. A requisição continua correndo até o fim, ocupando conexão, e se ela tiver efeito colateral no servidor — criar um pedido, cobrar um cartão — esse efeito acontece de qualquer maneira, mesmo que o cliente já tenha desistido. Cancelar de verdade é outro problema, e é exatamente o que o AbortController existe para resolver.

Exercício 4

A função tem cache. O que ela imprime na primeira chamada e o que imprime na segunda?

const cache = {};

function buscar(id, cb) {
  if (cache[id]) return cb(null, cache[id]);

  setTimeout(() => {
    cache[id] = { id, nome: "Produto" };
    cb(null, cache[id]);
  }, 100);
}

let pronto = false;
buscar(1, () => console.log("callback — pronto =", pronto));
pronto = true;
Ver resposta

✓ Resposta: Na primeira vez imprime pronto = true; na segunda, com o cache quente, imprime pronto = false. A mesma função chama o callback de forma assíncrona quando precisa buscar e de forma síncrona quando tem a resposta em mãos — e no caminho síncrono o callback roda antes da linha pronto = true, que ainda nem foi alcançada. O resultado é que qualquer estado preparado logo depois da chamada existe às vezes e às vezes não, e o bug aparece na segunda execução, quando o desenvolvedor já considerou o código pronto. Essa inconsistência é conhecida o bastante para ter apelido — "liberar Zalgo" — e a regra é categórica: um callback deve ser sempre assíncrono ou sempre síncrono, nunca depender do caminho tomado. Para forçar o caminho rápido a esperar, basta adiá-lo uma microtask: queueMicrotask(() => cb(null, cache[id])). As Promises eliminam a categoria inteira por construção, já que o .then nunca executa de forma síncrona, mesmo sobre uma promise já resolvida.

Exercício 5

Quais destes quatro poderiam ser trocados por uma Promise sem perder nada?

botao.addEventListener("click", cb); // A
setTimeout(cb, 1000);                // B
stream.on("data", cb);               // C
buscarUsuario(1, cb);                // D
Ver resposta

✓ Resposta: Só o B e o D. A diferença é quantas vezes o callback é chamado. Uma promise é um recipiente que se resolve exatamente uma vez e guarda esse valor para sempre: pedir de novo devolve o mesmo resultado, e não há como "resolvê-la de novo" com um valor novo. Isso serve perfeitamente para uma espera de tempo e para uma busca — operações únicas, que terminam em sucesso ou falha e acabam ali. Já um botão pode ser clicado cinquenta vezes e um stream entrega muitos pedaços: envolvê-los numa promise capturaria só o primeiro clique e o primeiro pedaço, e todo o resto se perderia sem aviso. Para eventos que se repetem, as ferramentas certas são o próprio callback, o iterador assíncrono com for await...of ou um observable. Vale ainda uma distinção que costuma se embaralhar aqui: a função passada para map ou filter também é um callback, e não tem nada de assíncrona — "callback" descreve como a função é entregue, não quando ela roda.

Comentários

Mais em Javascript

Criando um servidor HTTP com Node.js puro
Criando um servidor HTTP com Node.js puro

Um require de http, uma função de dois argumentos, e o servidor está de pé. O…

MongoDB e Mongoose: banco de dados com Node
MongoDB e Mongoose: banco de dados com Node

Um array na memória some quando o servidor reinicia, e é aí que entra o banco…

Deploy: do código ao ar
Deploy: do código ao ar

Construir é metade do trabalho; a outra metade é colocar no ar. O artigo…