Programação assíncrona em JavaScript

Programação assíncrona é um modelo de execução de programas no qual uma aplicação pode iniciar uma operação potencialmente demorada e continuar disponível enquanto aguarda sua conclusão. Esse comportamento é particularmente importante em aplicações web, pois operações de entrada e saída, tais como acesso à rede, leitura de arquivos, consultas a bancos de dados e comunicação com APIs, possuem tempos de resposta imprevisíveis.

No JavaScript moderno, esse modelo é construído principalmente sobre os conceitos de Promise, async e await. A especificação ECMAScript define formalmente essas construções, que permitem representar operações cujo resultado será conhecido no futuro (ECMA International, 2025).

A documentação da MDN resume o conceito de Promises como sendo um “objeto que representa a eventual conclusão (ou falha) de uma operação assíncrona” (MDN Web Docs, 2026). Esse objeto pode estar pendente, concluído com sucesso ou rejeitado em razão de uma falha (MDN Web Docs, 2026).

O problema da espera em aplicações web

Vamos começar considerando uma consulta qualquer a um banco de dados de uma loja virtual:

const produto = consultarProduto(10);

Visualmente, a instrução parece simples. Entretanto, uma consulta envolve comunicação entre processos, rede, processamento pelo SGBD, acesso a memória ou disco e transmissão da resposta. O resultado dificilmente está disponível no mesmo instante em que a função é chamada.

Em um modelo estritamente síncrono, a execução permaneceria bloqueada aguardando a resposta. Em servidores Node.js, isso é particularmente problemático, porque o Event Loop coordena numerosas requisições concorrentes. Bloqueá-lo por muito tempo reduz a quantidade de clientes que o servidor consegue atender (Node.js, 2026).

A ideia central neste caso é liberar o runtime para atender outras solicitações enquanto aguarda aquela operação retornar. Por exemplo, o fluxo seria similar ao descrito abaixo:

Requisição
    ↓
inicia operação de I/O
    ↓
O runtime pode atender outros trabalhos
    ↓
operação termina
    ↓
O runtime continua o processamento dependente daquele resultado

Um detalhe importante é que o fato de ser assíncrono não significa necessariamente executar duas instruções simultaneamente. O código assíncrono significa principalmente não bloquear desnecessariamente o fluxo de execução enquanto uma operação externa ainda não terminou.

Promises

Uma Promise é um objeto que representa um resultado futuro. Em JavaScript, ela possui três estados fundamentais: pending, enquanto a operação ainda não terminou; fulfilled, quando termina com sucesso; e rejected, quando ocorre falha (MDN Web Docs, 2026).

Um exemplo simplificado seria a busca de um produto:

function buscarProduto(id) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (id <= 0) {
                reject(new Error("ID inválido"));
                return;
            }

            resolve({
                id,
                nome: "Notebook",
                preco: 3500
            });
        }, 300);
    });
}

O método resolve() indica que a operação terminou corretamente, enquanto o reject() representa a ocorrência de uma falha.

O objeto Promise pode ser consumido com os métodos then() e catch(), tal como no exemplo abaixo:

buscarProduto(1)
    .then((produto) => {
        console.log(produto.nome);
    })
    .catch((erro) => {
        console.error(erro.message);
    });

Nesse caso, then() será executado quando a Promise for cumprida e catch() tratará sua rejeição. O encadeamento de Promises permite representar uma sequência de operações assíncronas sem recorrer a grandes estruturas de callbacks aninhados (MDN Web Docs, 2026).

Em aplicações maiores, contudo, cadeias extensas podem ficar visualmente difíceis de acompanhar, conforme o exemplo a seguir:

buscarProduto(1)
    .then((produto) => consultarVendedor(produto.vendedorId))
    .then((vendedor) => consultarAvaliacoes(vendedor.id))
    .then((avaliacoes) => console.log(avaliacoes))
    .catch((erro) => console.error(erro));

É exatamente nesse ponto que as palavras reservadas async e await tornam-se particularmente úteis.

Async e Await

Uma função declarada com async sempre retorna uma Promise. O operador await, por sua vez, suspende a continuação daquela função até que a Promise aguardada seja resolvida ou rejeitada. Porém, preciso reforçar que essa suspensão não equivale a bloquear todo o programa (MDN Web Docs, 2026).

O exemplo anterior pode ser reescrito:

async function exibirProduto(id) {
    try {
        const produto = await buscarProduto(id);

        console.log(
            `${produto.nome}: R$ ${produto.preco}`
        );
    } catch (erro) {
        console.error(
            "Falha ao buscar produto:",
            erro.message
        );
    }
}

await exibirProduto(1);

O código agora se aproxima visualmente do fluxo de um algoritmo sequencial:

buscar produto
      ↓
aguardar resposta
      ↓
usar produto

Essa legibilidade é uma das principais vantagens de async/await. A própria documentação da linguagem destaca que essas construções permitem escrever código assíncrono baseado em Promises com estrutura semelhante ao código síncrono (MDN Web Docs, 2026).

Existe, entretanto, uma regra importante: await deve normalmente ser utilizado dentro de uma função async. A exceção é o chamado top-level await, permitido em módulos JavaScript.

Por que usamos async/await tantas vezes em aplicações JavaScript?

A resposta não é simplesmente “porque JavaScript é assíncrono”. O motivo mais completo é que aplicações web executam continuamente operações de I/O.

No frontend, por exemplo, um comprador pode solicitar os produtos disponíveis usando fetch:

async function carregarProdutos() {
    const resposta = await fetch("/api/produtos");

    if (!resposta.ok) {
        throw new Error(
            `Erro HTTP: ${resposta.status}`
        );
    }

    const produtos = await resposta.json();

    return produtos;
}

O método fetch() retorna uma Promise porque a aplicação precisa esperar que a requisição atravesse a rede e que o servidor responda. Da mesma forma, a leitura do corpo por response.json() também é um acesso assíncrono (MDN Web Docs, 2026).

No backend ocorre situação semelhante. A consulta MySQL abaixo:

const [produtos] = await pool.execute(
    "SELECT * FROM produtos"
);

depende de um processo externo ao sistema. O runtime Node.js não conhece imediatamente o resultado, pois o driver retorna uma Promise, que será posteriormente cumprida ou rejeitada. Logo, é importante que indiquemos a função que chama essa consulta como assíncrona.

Assim, encontramos async/await constantemente em funções relacionadas a banco de dados, fetch, APIs REST, arquivos, autenticação, envio de mensagens e outros recursos de I/O. Porém, isso não significa que todas as funções devam ser async. Por exemplo:

function calcularTotal(preco, quantidade) {
    return preco * quantidade;
}

Essa função é puramente computacional. Transformá-la artificialmente em:

async function calcularTotal(preco, quantidade) {
    return preco * quantidade;
}

não traz benefício. Na verdade, passa a fazê-la retornar uma Promise sem necessidade.

Portanto, uma regra que podemos considerar é:

Use async/await quando a função precisa aguardar uma operação assíncrona; não transforme funções síncronas em assíncronas sem necessidade.

Um exemplo com compradores, vendedores e administradores

Considere uma função de autorização:

function autorizar(...tiposPermitidos) {
    return (req, res, next) => {
        if (!tiposPermitidos.includes(req.usuario.tipo)) {
            return res.status(403).json({
                erro: "Acesso não autorizado"
            });
        }

        next();
    };
}

Essa função não precisa de async. Ela apenas verifica uma informação que já está disponível em memória.

Já o cadastro de um produto exige comunicação com o MySQL:

app.post(
    "/produtos",
    autorizar("vendedor"),
    async (req, res) => {
        try {
            const { nome, preco, estoque } = req.body;

            const [resultado] = await pool.execute(
                `INSERT INTO produtos
                    (vendedor_id, nome, preco, estoque)
                 VALUES (?, ?, ?, ?)`,
                [
                    req.usuario.id,
                    nome,
                    preco,
                    estoque
                ]
            );

            return res.status(201).json({
                id: resultado.insertId,
                nome,
                preco,
                estoque
            });
        } catch (erro) {
            return res.status(500).json({
                erro: "Não foi possível cadastrar o produto"
            });
        }
    }
);

Aqui, async é necessário porque há um await sobre uma operação do banco.

Um administrador poderia igualmente consultar pedidos:

app.get(
    "/admin/pedidos",
    autorizar("admin"),
    async (req, res) => {
        try {
            const [pedidos] = await pool.execute(
                `SELECT id, comprador_id, status, total
                 FROM pedidos
                 ORDER BY id DESC`
            );

            return res.json(pedidos);
        } catch (erro) {
            return res.status(500).json({
                erro: "Erro ao consultar pedidos"
            });
        }
    }
);

O padrão se repete: operações locais simples permanecem síncronas, enquanto operações externas de I/O normalmente são assíncronas.

Tratamento de erros com async/await

Quando uma Promise é rejeitada e é utilizada com await, a rejeição comporta-se como uma exceção. Isso permite utilizar try...catch de maneira semelhante ao tratamento de erros em código síncrono (MDN Web Docs, 2026).

Leia o código a seguir:

async function consultarProduto(id) {
    try {
        const [produtos] = await pool.execute(
            `SELECT *
             FROM produtos
             WHERE id = ?`,
            [id]
        );

        if (produtos.length === 0) {
            throw new Error(
                "Produto não encontrado"
            );
        }

        return produtos[0];
    } catch (erro) {
        console.error(
            "Erro ao consultar produto:",
            erro.message
        );

        throw erro;
    }
}

O throw erro ao final é importante quando aquela camada não possui responsabilidade para decidir a resposta HTTP. O erro pode continuar subindo pela aplicação até uma camada apropriada. Podemos imaginar o seguinte fluxo:

Controller
    ↓
Service
    ↓
Repository
    ↓
MySQL

Como uma falha pode surgir no banco, devemos propagá-la pelo repositório até finalmente ser convertida pelo controller em uma resposta HTTP adequada ao cliente.

Operações sequenciais e concorrentes

O fato de estarmos usando await não significa que tudo deva ser aguardado sequencialmente. Imagine que o administrador de uma loja virtual queira recuperar simultaneamente três indicadores independentes:

const totalCompradores =
    await contarCompradores();

const totalVendedores =
    await contarVendedores();

const totalPedidos =
    await contarPedidos();

Nesse formato, a segunda operação somente começa depois da primeira, e a terceira depois da segunda. Se não houver dependência entre elas, podemos iniciar as três simultaneamente, conforme abaixo:

const [
    totalCompradores,
    totalVendedores,
    totalPedidos
] = await Promise.all([
    contarCompradores(),
    contarVendedores(),
    contarPedidos()
]);

A MDN recomenda esse tipo de abordagem quando operações assíncronas são independentes, pois aguardá-las sequencialmente introduz espera desnecessária (MDN Web Docs, 2026).

Portanto, descrevendo melhor o fluxo, temos:

Dependência entre operações
A → B → C
use await sequencialmente

Independência entre operações
A ─┐
B ─┼→ Promise.all()
C ─┘

JavaScript é a única linguagem que trabalha dessa maneira?

Resposta curta: não!

Resposta mais longa: o problema que async/await resolve não é exclusivo do JavaScript, pois aplicações web em praticamente qualquer linguagem precisam lidar com operações lentas de I/O. O que muda é o modelo de concorrência adotado pela plataforma (runtime + linguagem).

Em Python, o módulo asyncio utiliza corrotinas declaradas com async def e operações aguardáveis com await. Enquanto uma corrotina aguarda uma operação, o Event Loop pode executar outras tarefas (Python Software Foundation, 2026).

async def buscar_produto():
    produto = await consultar_banco()
    return produto

Em C#, async e await fazem parte do padrão Task-based Asynchronous Pattern. A própria Microsoft recomenda programação assíncrona principalmente para operações de I/O como acesso à rede, arquivos e bancos de dados (Microsoft, 2025).

async Task<Produto> BuscarProdutoAsync()
{
    Produto produto =
        await repositorio.BuscarAsync();

    return produto;
}

Java também possui recursos para representar computações assíncronas, como Future e CompletableFuture. Um CompletableFuture representa um resultado que poderá ser concluído posteriormente e permite associar etapas dependentes à sua conclusão (Oracle, 2026).

Portanto, o conceito de programação assíncrona é geral, ficando assim nas linguagens que apresentamos de exemplo:

JavaScript → Promise + async/await
Python     → coroutine + async/await
C#         → Task + async/await
Java       → Future / CompletableFuture

JavaScript apenas torna esse modelo especialmente visível porque navegadores e Node.js foram construídos fortemente em torno de programação orientada a eventos e I/O assíncrono.

Conclusão

Programação assíncrona é um fundamento da programação web moderna porque grande parte do tempo de uma aplicação não é consumida realizando cálculos, mas aguardando operações externas.

Em JavaScript, Promise é um objeto que representa o resultado futuro de uma operação. A palavra async indica uma função cujo resultado é uma Promise, enquanto await permite suspender o fluxo daquela função até que uma operação assíncrona seja concluída (ECMA International, 2025; MDN Web Docs, 2026).

Entretanto, async/await não deve ser usado mecanicamente em todas as funções. Seu emprego é especialmente apropriado para operações de I/O, como fetch, consultas a bancos de dados, arquivos e chamadas a serviços externos. Quando as funções são puramente computacionais (cálculos, comparações com dados na memória RAM etc.), elas deve continuar sendo síncronas.

Obrigado pela leitura e bons estudos!

Referências

ECMA INTERNATIONAL. ECMA-262: ECMAScript 2025 Language Specification. 16. ed. Geneva: Ecma International, 2025. Disponível em: ECMA-262. Acesso em: 15 set. 2026.

MDN WEB DOCS. Async function. [S. l.]: Mozilla, 2026. Disponível em: MDN — async function. Acesso em: 15 set. 2026.

MDN WEB DOCS. Promise. [S. l.]: Mozilla, 2026. Disponível em: MDN — Promise. Acesso em: 15 set. 2026.

MDN WEB DOCS. Using the Fetch API. [S. l.]: Mozilla, 2026. Disponível em: MDN — Fetch API. Acesso em: 15 set. 2026.

MICROSOFT. Asynchronous programming with async and await. Redmond: Microsoft, 2025. Disponível em: Microsoft Learn — async/await. Acesso em: 15 set. 2026.

NODE.JS. Don’t block the Event Loop (or the Worker Pool). 2026. Disponível em: Node.js — Don’t Block the Event Loop. Acesso em: 15 set. 2026.

ORACLE. CompletableFuture: Java Platform SE API. Austin: Oracle, 2026. Disponível em: Oracle Java API — CompletableFuture. Acesso em: 15 set. 2026.

PYTHON SOFTWARE FOUNDATION. Coroutines and Tasks. Python 3.14 documentation. 2026. Disponível em: Python — Coroutines and Tasks. Acesso em: 15 set. 2026