Optional chaining e nullish coalescing em JavaScript
Como o operador ?. devolve undefined em vez de quebrar, as versões para índice e chamada, e por que ?? não é a mesma coisa que || — com saída real no Node.
O operador ?. lê uma propriedade e, se o valor da esquerda for null ou
undefined, para no meio do caminho e devolve undefined em vez de lançar
erro. O operador ?? escolhe um valor de reserva, mas só quando o da esquerda é
null ou undefined — e é aí que ele se separa do ||, que aceita qualquer
valor falso como ausência.
Resposta curta: ?. protege a leitura, ?? preenche o buraco. Os dois
tratam null e undefined como “não veio” e ignoram 0, '' e false, que
são valores de verdade.
Toda a lição roda sobre a mesma resposta de API de um checkout: nem todo pedido tem endereço de entrega, nem todo cliente tem cupom.
Corrimão e valor reserva: dois trabalhos diferentes
Num prédio, o corrimão impede que você caia quando a escada acaba; ele não
constrói o andar que está faltando. O ?. faz esse papel na leitura de dados:
interrompe o caminho com segurança. O ?? entra depois como uma placa de
reserva — “endereço não informado”, por exemplo.
Tecnicamente, optional chaining faz curto-circuito apenas diante de null
ou undefined, e nullish coalescing escolhe o operando da direita nas mesmas
duas ausências. No primeiro pedido, siga o caminho propriedade por propriedade
e marque onde ele para; em seguida confira se o valor reserva aparece sem
apagar 0, false ou string vazia.
const pedido = {
numero: 8412,
cliente: { nome: 'Ana Souza' },
};
console.log(pedido.cliente.endereco.cidade);Leia o acento circunflexo: ele aponta para cidade, mas a culpada é endereco.
A mensagem diz “não consigo ler cidade de undefined” — quem estava
undefined era o valor imediatamente à esquerda do ponto. É o mesmo diagnóstico
detalhado em Cannot read properties of undefined.
Antes de 2020, a defesa era uma escada de &&. Hoje é uma interrogação:
const pedido = {
numero: 8412,
cliente: { nome: 'Ana Souza' },
};
console.log(pedido.cliente.endereco?.cidade);
console.log(pedido.entrega?.rastreio?.codigo);
console.log(pedido.cliente?.nome);Três coisas para observar. A primeira linha não quebrou. A segunda mostra que
vários ?. se encadeiam sem problema. A terceira mostra que, quando o valor
existe, o operador simplesmente sai do caminho.
Curto-circuito: o resto da expressão nem chega a rodar
Isso não é um “if disfarçado” que testa cada pedaço. Quando o operador encontra
null ou undefined, ele abandona a expressão inteira dali para a direita:
function buscarCampo() {
console.log('consultou a API de endereço');
return 'cidade';
}
const pedido = { numero: 8412, cliente: { nome: 'Ana Souza' } };
console.log(pedido.cliente.endereco?.[buscarCampo()].toUpperCase());
console.log('terminou sem chamar a API e sem quebrar');buscarCampo() nunca rodou — a mensagem dela não apareceu. E .toUpperCase(),
que estava depois de tudo e seria chamado sobre undefined, também foi pulado.
Esse comportamento tem nome: curto-circuito. É o mesmo do &&, e é o que faz o
operador valer a pena em cadeias longas.
Três formas: propriedade, índice e chamada
O ?. aparece em três posições, e a diferença é onde entra o ponto:
const carrinho = {
itens: [{ sku: 'TEC-01', preco: 289.9 }],
};
const carrinhoVazio = {};
console.log(carrinho.itens?.[0]?.sku);
console.log(carrinhoVazio.itens?.[0]?.sku);
const campo = 'preco';
console.log(carrinho.itens?.[0]?.[campo]);
const analytics = { registrar: (evento) => `evento: ${evento}` };
const analyticsDesligado = {};
console.log(analytics.registrar?.('checkout'));
console.log(analyticsDesligado.registrar?.('checkout'));| forma | escreve-se | protege contra |
|---|---|---|
| propriedade | obj?.chave |
obj ser null ou undefined |
| índice ou chave dinâmica | obj?.[expressao] |
o mesmo, mas com colchetes |
| chamada | fn?.(args) |
a função não existir naquele objeto |
A forma de chamada é a menos conhecida e a mais útil em callback opcional: um
onSucesso?.(pedido) roda o callback se ele foi passado e não faz nada se não
foi — sem if, sem valor padrão vazio.
?? não é ||
Os dois escolhem um valor de reserva. A diferença é o que cada um considera “vazio”:
const configuracoes = [
{ rotulo: 'frete grátis', desconto: 0 },
{ rotulo: 'observação vazia', observacao: '' },
{ rotulo: 'presente desmarcado', presente: false },
{ rotulo: 'cupom nulo', cupom: null },
];
console.log(configuracoes[0].desconto || 15, '|', configuracoes[0].desconto ?? 15);
console.log(`[${configuracoes[1].observacao || 'sem observação'}]`, '|', `[${configuracoes[1].observacao ?? 'sem observação'}]`);
console.log(configuracoes[2].presente || true, '|', configuracoes[2].presente ?? true);
console.log(configuracoes[3].cupom || 'PRIMEIRACOMPRA', '|', configuracoes[3].cupom ?? 'PRIMEIRACOMPRA');Nas três primeiras linhas o || mentiu. Ele trocou um desconto de zero por
quinze, encheu uma observação que o cliente deixou em branco de propósito e
marcou como presente um pedido que não era. Só na quarta linha, em que o valor
era mesmo null, os dois concordaram.
Num checkout isso vira dinheiro:
function resumoComOu(pedido) {
const valor = pedido.frete || 24.9;
return `frete: R$ ${valor.toFixed(2)}`;
}
function resumoComNullish(pedido) {
const valor = pedido.frete ?? 24.9;
return `frete: R$ ${valor.toFixed(2)}`;
}
console.log(resumoComOu({ numero: 8412, frete: 0 }));
console.log(resumoComNullish({ numero: 8412, frete: 0 }));
console.log(resumoComNullish({ numero: 8413 }));O pedido tinha frete grátis e a primeira função cobrou R$ 24,90 do cliente. Esse bug é chato de achar porque o código parece certo e só falha quando o valor é exatamente zero — que costuma ser o caso raro em teste e o caso comum em produção.
??= preenche só o que está faltando
A atribuição de coalescência nula escreve na propriedade apenas quando ela é
null ou undefined:
const preferencias = {
embalagemPresente: false,
parcelas: 0,
entrega: null,
};
preferencias.embalagemPresente ??= true;
preferencias.parcelas ??= 1;
preferencias.entrega ??= 'padrão';
preferencias.notaFiscal ??= 'e-mail';
console.log(preferencias);As escolhas do cliente (false e 0) sobreviveram; o que estava nulo e o que
nem existia receberam o padrão. Com ||= no lugar, as duas primeiras linhas
teriam atropelado a decisão de quem comprou.
Misturar ?? com || é erro de sintaxe
A linguagem não deixa você combinar os dois sem parênteses — de propósito, para que ninguém precise adivinhar a precedência:
const pedido = { cupom: null, frete: 0 };
const valor = pedido.cupom ?? pedido.frete || 24.9;
console.log(valor);Repare que nenhuma linha rodou: é erro de sintaxe, detectado antes da execução. Com parênteses, você escolhe o significado — e os dois significados são diferentes:
const pedido = { cupom: null, frete: 0 };
console.log((pedido.cupom ?? pedido.frete) || 24.9);
console.log(pedido.cupom ?? (pedido.frete || 24.9));Aqui o resultado calhou de ser igual, e é justamente por isso que a linguagem obriga os parênteses: com outro dado, não seria. Escreva o que você quer dizer.
Erros comuns
Tentar escrever através de um ?.
const pedido = { numero: 8412 };
pedido.entrega?.cidade = 'São Paulo';Optional chaining é só para leitura. Para escrever, garanta o objeto primeiro:
(pedido.entrega ??= {}).cidade = 'São Paulo'.
Usar ?. para calar um bug
Este é o custo real de espalhar interrogações: o erro deixa de aparecer no lugar onde foi cometido.
const carrinho = { produtos: [{ sku: 'TEC-01', preco: 289.9 }] };
const total = carrinho.itens?.reduce((soma, item) => soma + item.preco, 0);
console.log('total:', total);
console.log('com frete:', total + 12.5);
console.log('formatado:', `R$ ${Number(total).toFixed(2)}`);A propriedade certa era produtos, não itens — um typo. Sem o ?., o
programa teria parado na linha do erro com
TypeError: Cannot read properties of undefined (reading 'reduce'), apontando
o dedo para a linha do typo. Com ele, o undefined viajou três linhas e virou
R$ NaN na tela do cliente.
O padrão que resolve resposta de API
Juntando as duas peças, a leitura de uma resposta com campos opcionais fica numa
linha por campo, sem if aninhado:
const respostas = [
{
numero: 8412,
cliente: { nome: 'Ana Souza', endereco: { cidade: 'São Paulo', uf: 'SP' } },
cupom: { codigo: 'PRIMEIRACOMPRA', desconto: 10 },
},
{ numero: 8413, cliente: { nome: 'Bruno Lima' } },
];
for (const pedido of respostas) {
const cidade = pedido.cliente?.endereco?.cidade ?? 'cidade não informada';
const desconto = pedido.cupom?.desconto ?? 0;
console.log(`#${pedido.numero} — ${pedido.cliente?.nome} · ${cidade} · ${desconto}% de desconto`);
}O par ?. seguido de ?? é a dupla que você vai escrever mais vezes na vida:
um garante que a leitura não quebra, o outro decide o que aparece quando não
tem. Quando os campos vêm de um objeto que você vai abrir de qualquer jeito,
compare com o ?? {} da lição de
desestruturação, spread e rest —
resolvem o mesmo problema em pontos diferentes do código.
Continue pela trilha de JavaScript: a próxima parada é JSON, que é de onde esses objetos cheios de campo opcional costumam vir. O guia completo de JavaScript tem o mapa inteiro.
Perguntas frequentes
Optional chaining deixa o código mais lento?
Qual a diferença entre ?. e o operador && encadeado?
Posso usar ?. do lado esquerdo de uma atribuição?
Quando ?? é melhor que ||?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Encadeamento opcional (?.) — developer.mozilla.org
- MDN — Operador de coalescência nula (??) — developer.mozilla.org


