Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

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.

Rodolfo Mori6 min de leitura

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.

js
const pedido = {
  numero: 8412,
  cliente: { nome: 'Ana Souza' },
};

console.log(pedido.cliente.endereco.cidade);
file:///private/tmp/checkout/endereco-sem-protecao.mjs:6 console.log(pedido.cliente.endereco.cidade); ^ TypeError: Cannot read properties of undefined (reading 'cidade') at file:///private/tmp/checkout/endereco-sem-protecao.mjs:6:37 Node.js v24.16.0

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:

js
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);
undefined undefined Ana Souza

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:

js
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');
undefined 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:

js
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'));
TEC-01 undefined 289.9 evento: checkout undefined
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”:

js
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');
15 | 0 [sem observação] | [] true | false PRIMEIRACOMPRA | 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:

js
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 }));
frete: R$ 24.90 frete: R$ 0.00 frete: R$ 24.90

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:

js
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);
{ embalagemPresente: false, parcelas: 0, entrega: 'padrão', notaFiscal: 'e-mail' }

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:

js
const pedido = { cupom: null, frete: 0 };

const valor = pedido.cupom ?? pedido.frete || 24.9;

console.log(valor);
file:///private/tmp/checkout/misturar-operadores.mjs:3 const valor = pedido.cupom ?? pedido.frete || 24.9; ^^ SyntaxError: Unexpected token '||' Node.js v24.16.0

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:

js
const pedido = { cupom: null, frete: 0 };

console.log((pedido.cupom ?? pedido.frete) || 24.9);
console.log(pedido.cupom ?? (pedido.frete || 24.9));
24.9 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 ?.

js
const pedido = { numero: 8412 };

pedido.entrega?.cidade = 'São Paulo';
file:///private/tmp/checkout/opcional-na-atribuicao.mjs:3 pedido.entrega?.cidade = 'São Paulo'; ^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Invalid left-hand side in assignment Node.js v24.16.0

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.

js
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)}`);
total: undefined com frete: NaN formatado: R$ NaN

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:

js
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`);
}
#8412 — Ana Souza · São Paulo · 10% de desconto #8413 — Bruno Lima · cidade não informada · 0% 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.

  • optional chaining
  • nullish coalescing
  • undefined
  • null
  • operadores

Perguntas frequentes

Optional chaining deixa o código mais lento?
Na prática, não. O operador vira uma comparação com null que os motores modernos otimizam; o custo é o mesmo de um if que você escreveria à mão. O problema de espalhar interrogação pelo código é de legibilidade e de bug escondido, não de desempenho.
Qual a diferença entre ?. e o operador && encadeado?
O && para no primeiro valor falso — zero, string vazia e false incluídos — e devolve esse valor. O ?. para só em null e undefined, e devolve sempre undefined. Por isso o checkout que testava frete && frete.valor errava justamente quando o frete era grátis.
Posso usar ?. do lado esquerdo de uma atribuição?
Não. É erro de sintaxe, e o motor recusa antes de executar qualquer linha. Optional chaining é para leitura; para escrever com segurança você precisa garantir o objeto antes, com ?? ou com um if.
Quando ?? é melhor que ||?
Sempre que zero, string vazia e false forem valores legítimos no seu domínio — frete grátis, desconto zerado, observação em branco, checkbox desmarcado. Use || só quando qualquer valor falso realmente significar ausência, o que é mais raro do que parece.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — Encadeamento opcional (?.) — developer.mozilla.org
  2. MDN — Operador de coalescência nula (??) — developer.mozilla.org

Continue por aqui