Pular para o conteúdo

JavaScript por dentro: a linguagem da web, do navegador ao servidor

Como o JavaScript nasceu, como os motores o executam, o que são o event loop, os protótipos e os módulos, e por que coerção, this e async ainda confundem tanta gente. Uma imersão com exemplos, erros comuns, segurança, TypeScript e o jeito como usamos a linguagem na Gothor.

Equipe Gothor· Publicado em · Atualizado em · 45 min de leitura
Capa do artigo com o título JavaScript, a linguagem da web, e um editor com um exemplo de event loop e a ordem de saída no console

Toda página que você abre no navegador executa JavaScript. O menu que se expande, a busca que sugere resultados enquanto você digita, o carrinho que atualiza sem recarregar a página: tudo isso é JavaScript. E a linguagem não ficou só no navegador. Ela roda em servidores, em ferramentas de linha de comando, em aplicativos de desktop e em boa parte das ferramentas que constroem a própria web.

Ao mesmo tempo, poucas linguagens são tão usadas e tão mal compreendidas. Muita gente escreve JavaScript há anos sem saber por que 0.1 + 0.2 não dá 0.3, por que um this "se perde" ou em que ordem um setTimeout e uma Promise vão rodar. Este artigo percorre a linguagem de dentro para fora: a história, o motor, os fundamentos que mais confundem, o modelo assíncrono, os módulos, o TypeScript e as práticas que evitam os erros mais caros.

O que é JavaScript

JavaScript é uma linguagem de programação dinâmica, de tipagem fraca, multiparadigma e executada por um motor embutido num ambiente, como um navegador ou o Node.js. Cada uma dessas palavras tem consequências:

  • Dinâmica: os tipos são verificados durante a execução, e não antes. Uma variável pode guardar um número agora e um texto depois.
  • Tipagem fraca: em muitas operações, a linguagem converte valores de um tipo para outro automaticamente, o que é prático e, às vezes, surpreendente.
  • Multiparadigma: funções são valores comuns, que podem ser passadas e retornadas, o que permite um estilo funcional; objetos e protótipos permitem um estilo orientado a objetos.
  • Uma thread de execução: o código de uma página ou de um processo Node roda numa única linha de execução, e a concorrência acontece pelo event loop, que veremos em detalhe.

Dois esclarecimentos de nome. JavaScript não tem relação técnica com Java; o nome foi uma decisão de marketing dos anos 1990. E o nome oficial do padrão é ECMAScript: JavaScript é a linguagem que implementa esse padrão, com o nome que todo mundo usa.

Uma história em dez dias, e trinta anos

Em 1995, a Netscape queria uma linguagem de script simples para o seu navegador, algo que designers e programadores iniciantes pudessem usar direto no HTML. Brendan Eich escreveu o primeiro protótipo em cerca de dez dias. A linguagem se chamou Mocha, depois LiveScript e, no lançamento, JavaScript, aproveitando a fama do Java naquele momento.

Linha do tempo do JavaScript de 1995 a 2023
Três décadas de JavaScript, da Netscape aos runtimes de servidor

A Microsoft lançou a sua própria versão, chamada JScript, e para evitar que a web se dividisse em dialetos a Netscape levou a linguagem para padronização na Ecma International. Em 1997 saiu a primeira edição do ECMA-262. A terceira edição, de 1999, trouxe expressões regulares e tratamento de exceções, e serviu de base por uma década. Uma quarta edição muito ambiciosa foi abandonada em 2008, por falta de consenso.

A década seguinte mudou o papel da linguagem. Em 2005, o termo Ajax deu nome a páginas que buscam dados no servidor sem recarregar, e a web passou a ter aplicações de verdade. Em 2006 veio o jQuery, que domou as diferenças entre navegadores. Em 2008, o Google lançou o Chrome com o motor V8, que compilava JavaScript para código de máquina e iniciou uma corrida de desempenho entre os navegadores. Em 2009 saíram o ES5, com modo estrito e JSON nativo, e o Node.js, que levou o V8 para fora do navegador. Em 2010 nasceu o npm.

Em 2015 veio a maior atualização da história da linguagem, o ES2015 (também chamado de ES6), com classes, módulos, let e const, arrow functions, Promises, desestruturação e muito mais. Desde então, o comitê TC39, que reúne fabricantes de navegadores, empresas e especialistas, publica uma edição por ano. Cada novidade passa por estágios, de uma ideia inicial até a aprovação final, que exige implementações reais em mais de um motor.

A quase divisão da linguagem

Vale olhar com mais calma para os anos 2000, porque eles explicam muito do JavaScript de hoje. Durante a chamada guerra dos navegadores, a Netscape e a Microsoft acrescentavam recursos por conta própria, e cada uma implementava o padrão do seu jeito. Escrever um script que funcionasse nos dois navegadores exigia testes do tipo "se for o Internet Explorer, faça assim; senão, faça assado". Bibliotecas como o Prototype, o MooTools e, depois, o jQuery nasceram justamente para esconder essas diferenças.

A quarta edição do ECMAScript tentava resolver tudo de uma vez: tipos estáticos opcionais, classes, pacotes, namespaces. Era quase outra linguagem, e parte do comitê temia que ela quebrasse a web existente. Em 2008, numa reunião em Oslo, o grupo chegou a um acordo que ficou conhecido como Harmony: abandonar a quarta edição, publicar uma atualização modesta (a quinta, de 2009) e levar as ideias grandes adiante aos poucos, em edições futuras. Boa parte do que a quarta edição prometia chegou, com outra forma, no ES2015. E a regra que saiu dessa crise vale até hoje: não quebrar a web. Um recurso que funciona num site de 1998 tem de continuar funcionando, e é por isso que esquisitices como typeof null nunca foram corrigidas.

O Node.js e a virada para o servidor

Quando Ryan Dahl apresentou o Node.js, em 2009, a maior parte dos servidores web criava uma thread ou um processo para cada conexão. Uma conexão parada esperando o banco de dados ou o disco ocupava memória sem fazer nada. A proposta do Node era outra: uma única thread, que nunca fica parada esperando entrada e saída, e um laço de eventos que retoma cada trabalho quando a resposta chega. JavaScript era a linguagem ideal para isso, porque já nascera orientada a eventos no navegador e não carregava bibliotecas de entrada e saída bloqueantes.

O modelo pegou. O npm surgiu no ano seguinte, a comunidade cresceu muito rápido e, em 2014, uma divergência sobre o ritmo das versões levou a um fork, o io.js, que voltou a se juntar ao projeto principal em 2015, sob uma fundação independente. Desde então, o Node lança uma versão nova a cada seis meses, e as versões pares ganham suporte de longo prazo (LTS), que é o que se usa em produção.

Uma edição por ano

Desde 2015, as edições anuais trazem poucas novidades de cada vez, o que torna a evolução previsível. Algumas das mais usadas no dia a dia:

EdiçãoDestaques
ES2016operador ** (potência) e Array.prototype.includes
ES2017async/await, Object.entries e Object.values, padStart
ES2018espalhamento em objetos, Promise.prototype.finally, for await...of, grupos nomeados em expressões regulares
ES2019flat e flatMap, Object.fromEntries, catch sem variável
ES2020bigint, ?., ??, Promise.allSettled, import() dinâmico, globalThis
ES2021replaceAll, Promise.any, ??= e `
ES2022await no topo do módulo, campos privados com #, .at(), Object.hasOwn, cause nos erros
ES2023toSorted, toReversed, toSpliced e with, que não alteram o array original; findLast
ES2024Object.groupBy e Map.groupBy, Promise.withResolvers
ES2025métodos de iteradores (map, filter, take direto num iterador), operações de conjunto (union, intersection), importação de JSON

O processo de estágios explica por que um recurso aparece num navegador antes de estar no padrão: os motores implementam as propostas avançadas, os desenvolvedores testam, e só então a edição seguinte as incorpora. Para quem escreve código, a pergunta prática é sempre a mesma: o ambiente em que meu código vai rodar já suporta isso?

Como o JavaScript roda: motor e ambiente

Para entender JavaScript, é preciso separar duas camadas que costumam ser confundidas: a linguagem, definida pelo ECMAScript, e o ambiente em que ela roda, que fornece tudo o que não é linguagem.

Diagrama com a linguagem ECMAScript, os motores V8, SpiderMonkey e JavaScriptCore e os ambientes navegador, Node.js, Deno e Bun
A linguagem é a mesma; motores e ambientes mudam

A linguagem define tipos, objetos, funções, classes, Promises e módulos. Ela não define como mostrar algo na tela, ler um arquivo ou fazer uma requisição de rede. Isso vem do ambiente:

  • No navegador, o ambiente oferece o DOM (a página como objetos), eventos de mouse e teclado, fetch, armazenamento local, Canvas e Web Workers.
  • No Node.js, oferece acesso a arquivos, rede, processos e variáveis de ambiente, com a biblioteca libuv cuidando da entrada e saída.
  • Deno e Bun são runtimes de servidor mais novos. O Deno, criado pelo autor original do Node, executa TypeScript diretamente e exige permissões explícitas para acessar disco e rede. O Bun usa o motor JavaScriptCore e traz gerenciador de pacotes, empacotador e executor de testes embutidos.

Os motores são os programas que executam a linguagem: o V8 (Chrome, Edge, Node.js e Deno), o SpiderMonkey (Firefox) e o JavaScriptCore (Safari e Bun).

Do texto ao código de máquina

Os motores modernos não são simples interpretadores. No V8, por exemplo, o código é analisado e transformado numa árvore sintática, que vira bytecode executado por um interpretador (o Ignition). Enquanto o código roda, o motor coleta informações sobre ele: que tipos chegam em cada função, que formatos de objeto aparecem. Funções muito usadas são então compiladas para código de máquina otimizado (pelo TurboFan, entre outros níveis), com base nessas observações. Se uma suposição deixa de valer, por exemplo uma função que sempre recebeu números passa a receber textos, o motor desotimiza e volta ao bytecode.

Duas consequências práticas: código que trata dados de forma consistente (objetos criados sempre com as mesmas propriedades, na mesma ordem; funções chamadas sempre com os mesmos tipos) tende a ser mais rápido; e microtruques de desempenho raramente valem a pena, porque o motor já é muito bom em otimizar código simples e previsível.

Formas ocultas e caches em linha

Como o motor consegue acelerar uma linguagem em que qualquer objeto pode ganhar ou perder propriedades a qualquer momento? Uma das técnicas centrais são as formas ocultas (no V8, chamadas internamente de maps; em outros motores, de shapes). Cada objeto aponta para uma descrição do seu formato: quais propriedades tem, em que ordem e em que posição da memória cada uma fica. Objetos criados do mesmo jeito compartilham a mesma forma.

Com isso, o motor pode usar caches em linha: no ponto do código em que se lê pedido.total, ele lembra que, da última vez, o objeto tinha a forma X e o total estava na posição 3. Se o próximo objeto tiver a mesma forma, a leitura vira um acesso direto à memória, sem procurar nada. Um ponto do código que sempre vê a mesma forma é chamado de monomórfico e é o caso mais rápido; com algumas formas diferentes, vira polimórfico; com muitas, megamórfico, e o motor desiste do atalho.

function ponto(x, y) { return { x, y }; }  // sempre o mesmo formato, na mesma ordem

const a = ponto(1, 2);
const b = ponto(3, 4);       // a e b compartilham a forma
b.z = 5;                     // b passa a ter outra forma
delete a.x;                  // a costuma virar um "dicionário", de acesso mais lento

Nada disso é motivo para escrever código estranho. A lição prática é a mesma de antes: crie objetos de um mesmo tipo sempre com as mesmas propriedades (classes e funções de fábrica ajudam), prefira atribuir undefined ou null a usar delete em objetos usados em laços quentes, e não misture tipos no mesmo array quando ele for grande e muito percorrido.

Memória e coleta de lixo

A memória é gerenciada automaticamente. O coletor de lixo encontra objetos que não são mais alcançáveis a partir das variáveis em uso e libera o espaço. Por isso, vazamentos de memória em JavaScript quase sempre são referências esquecidas: um cache global que só cresce, um listener de evento que nunca é removido, uma closure que segura um objeto grande, um intervalo de tempo que nunca é cancelado.

Os coletores modernos são geracionais, porque a maior parte dos objetos morre jovem: o objeto temporário de um cálculo, a string montada para um log, o resultado intermediário de um map. No V8, os objetos novos nascem numa área pequena, a geração jovem, que é varrida com frequência e muito rapidamente: só os sobreviventes são copiados adiante. Quem sobrevive a duas varreduras é promovido à geração antiga, que é marcada e compactada com menos frequência, em boa parte do tempo em paralelo e em pequenos passos, para não congelar a página ou o servidor. Ainda assim, um programa que cria muito lixo paga em pausas e em CPU, e um processo Node tem um teto de memória para a geração antiga (ajustável com a opção --max-old-space-size).

A linguagem oferece ferramentas para guardar dados sem impedir a coleta. Um WeakMap associa informações a um objeto sem segurá-lo: quando o objeto deixa de ser usado em outro lugar, a entrada some junto. É o jeito certo de criar um cache por objeto, por exemplo os metadados de cada elemento da página.

Como achar um vazamento

Quando a memória de uma página ou de um servidor só cresce, o caminho costuma ser este:

  1. Confirme o padrão. No Node, process.memoryUsage() mostra o heap em uso; num gráfico ao longo de horas, um vazamento aparece como uma escada que nunca desce, mesmo depois das coletas.
  2. Tire dois retratos da memória (heap snapshots), um no início e outro depois de repetir várias vezes a ação suspeita. No navegador, isso fica na aba Memory das ferramentas do desenvolvedor; no Node, basta iniciar o processo com --inspect e conectar as mesmas ferramentas.
  3. Compare os retratos e ordene pelo que mais cresceu. A ferramenta mostra a cadeia de referências que mantém cada objeto vivo, o que normalmente aponta o culpado: um array global, um mapa de sessões sem expiração, um evento registrado a cada abertura de tela e nunca removido.
  4. Corrija na origem: limite o tamanho do cache, dê prazo de validade às entradas, remova os listeners ao desmontar um componente, cancele os intervalos.

Os fundamentos que mais confundem

Tipos e coerção

JavaScript tem sete tipos primitivos (string, number, bigint, boolean, undefined, symbol e null) e os objetos, que incluem arrays e funções. Duas peculiaridades históricas aparecem logo: typeof null retorna "object", um erro da primeira versão que nunca pôde ser corrigido sem quebrar a web, e todos os números comuns são de ponto flutuante de 64 bits, o padrão IEEE 754.

Desse padrão vem o famoso 0.1 + 0.2 === 0.30000000000000004. Não é um defeito do JavaScript: acontece em qualquer linguagem que use ponto flutuante binário. Para valores monetários, trabalhe com centavos em números inteiros (ou bigint) e formate só na exibição.

A coerção é a conversão automática de tipos. Ela aparece principalmente no operador == e no +:

ExpressãoResultadoPor quê
'1' == 1trueo texto é convertido para número
0 == ''trueo texto vazio vira 0
null == undefinedtrueregra especial do ==
'1' + 1'11'com um texto, o + concatena
'3' * '4'12o * só existe para números
NaN === NaNfalseNaN não é igual a nada, nem a si mesmo
[] + {}'[object Object]'os dois viram texto antes de somar

A regra prática é simples: **use sempre === e !==**, que comparam sem converter. Para testar NaN, use Number.isNaN(valor). A única exceção aceita por muitos manuais de estilo é valor == null, que testa null e undefined de uma vez.

Verdadeiro, falso e os valores "vazios"

Em condições como if (valor), todo valor é tratado como verdadeiro ou falso. A lista dos valores falsos é curta e vale decorar: false, 0, -0, 0n, '' (texto vazio), null, undefined e NaN. Todo o resto é verdadeiro, inclusive '0', 'false', [] e {}.

Isso causa um erro frequente: if (quantidade) trata uma quantidade zero como se não existisse, e quantidade || 1 troca o zero por um. Quando zero ou texto vazio são valores legítimos, compare explicitamente (quantidade !== undefined) ou use o ??, que só troca null e undefined.

Valores e referências

Os primitivos são copiados por valor: let b = a com um número cria uma cópia independente. Objetos, arrays e funções são acessados por referência: const b = a com um objeto faz as duas variáveis apontarem para o mesmo objeto, e alterar b.status altera o que a enxerga.

const original = { cliente: 'Ana', itens: [{ preco: 10 }] };

const rasa = { ...original };          // copia só o primeiro nível
rasa.itens.push({ preco: 5 });         // altera também original.itens

const profunda = structuredClone(original); // copia tudo, em qualquer profundidade
profunda.itens.push({ preco: 7 });     // original continua igual

Duas consequências práticas. Primeiro, comparar objetos com === compara a referência, não o conteúdo: { a: 1 } === { a: 1 } é false. Segundo, uma função que recebe um objeto pode alterá-lo sem que quem chamou perceba. Por isso o estilo moderno favorece criar objetos novos em vez de alterar os existentes, e Object.freeze ajuda a proteger configurações, lembrando que ele também congela só o primeiro nível.

Números, dinheiro e formatação

Além do problema das casas decimais, o ponto flutuante de 64 bits tem um limite para inteiros: até Number.MAX_SAFE_INTEGER, que é 9.007.199.254.740.991, todo inteiro é representado com exatidão; acima disso, números vizinhos passam a ser confundidos. Isso importa quando um banco de dados usa identificadores de 64 bits: um id muito grande que chega como número pode ser arredondado em silêncio. A solução é trafegar esses ids como texto ou usar bigint (9007199254740993n), que tem precisão ilimitada mas não se mistura com number nas contas.

Para dinheiro, o caminho seguro é fazer as contas em centavos inteiros e arredondar uma vez, no ponto certo. Desconfie do toFixed para arredondar: ele trabalha sobre o número binário e devolve texto, por isso (1.005).toFixed(2) resulta em '1.00', e não em '1.01'. Para exibir, a API Intl resolve formato, separadores e moeda conforme o idioma:

const reais = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
reais.format(1234.5);                          // 'R$ 1.234,50'

const centavos = 19990;                        // o valor guardado
reais.format(centavos / 100);                  // 'R$ 199,90', só na exibição

Textos e Unicode

Os textos de JavaScript são sequências de unidades de 16 bits (UTF-16). Para quase todo o português isso é transparente, mas alguns caracteres, como muitos emojis, ocupam duas unidades: '😀'.length é 2. Para contar caracteres como uma pessoa contaria, use [...texto].length, que percorre o texto por caractere.

Acentos têm outra pegadinha: "ã" pode ser gravado como um caractere só ou como "a" seguido de um til combinável. Os dois aparecem iguais na tela e são diferentes para o ===. O método normalize resolve: normalize('NFC') junta tudo na forma composta, e normalize('NFD') separa as letras dos acentos, o que permite removê-los com uma expressão regular. É assim que muitos sistemas geram endereços amigáveis a partir de títulos, transformando "Boleto híbrido" em boleto-hibrido. Para ordenar nomes em português, use localeCompare ou Intl.Collator('pt-BR'), que sabem que "Álvaro" vem antes de "Bruno".

Datas e fusos horários

O objeto Date é uma das partes mais antigas e mais criticadas da linguagem. Ele é mutável, os meses começam em zero (new Date(2026, 0, 15) é 15 de janeiro) e o mesmo texto pode ser lido de formas diferentes: new Date('2026-10-20') é interpretado como meia-noite em UTC, e para quem está em Cuiabá, quatro horas atrás, essa data aparece como 19 de outubro às 20h.

Algumas regras evitam a maioria dos problemas: guarde e transmita datas em UTC, no formato ISO 8601 (2026-10-20T10:00:00Z); converta para o fuso de quem lê só na exibição; e use Intl.DateTimeFormat com o fuso explícito quando o servidor e o usuário estão em lugares diferentes:

new Intl.DateTimeFormat('pt-BR', {
  dateStyle: 'short',
  timeStyle: 'short',
  timeZone: 'America/Cuiaba',
}).format(new Date('2026-10-20T10:00:00Z'));   // '20/10/2026, 06:00'

A proposta Temporal, do TC39, traz tipos imutáveis e separados para cada caso (uma data sem hora, uma hora sem data, um instante, uma data com fuso) e já começou a chegar aos navegadores. Até ela estar em todo lugar, bibliotecas como a date-fns cobrem as contas com datas.

Escopo, hoisting e closures

Há três formas de declarar variáveis. O var, a mais antiga, tem escopo de função e é "içado" (hoisting) para o topo da função, podendo ser usado antes da declaração, com o valor undefined. O let e o const, de 2015, têm escopo de bloco e não podem ser usados antes da declaração. O const impede reatribuir a variável, mas não impede alterar o objeto para o qual ela aponta.

Uma closure é uma função que lembra as variáveis do lugar onde foi criada, mesmo depois que esse lugar terminou de executar. É um dos recursos mais poderosos da linguagem:

function criarContador() {
  let total = 0;               // só a função de dentro enxerga esta variável
  return () => ++total;
}

const proximo = criarContador();
proximo(); // 1
proximo(); // 2

Closures dão privacidade sem classes, são a base de callbacks e de hooks como os do React e explicam um erro clássico. Com var, só existe uma variável para o laço inteiro:

for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3, 3, 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j)); // 0, 1, 2

Com let, cada volta do laço tem a sua própria variável, e cada closure guarda a sua. Na prática: use const por padrão, let quando precisar reatribuir e não use var em código novo.

O valor de this

Em JavaScript, this não depende de onde a função foi escrita, e sim de como foi chamada:

  • Chamada como método (pedido.total()), this é o objeto antes do ponto.
  • Chamada solta (const f = pedido.total; f()), this é undefined em modo estrito, e o método "perde" o objeto.
  • Chamada com new, this é o objeto novo.
  • Com call, apply ou bind, this é o que você indicar.

As arrow functions são a exceção: elas não têm this próprio e usam o do lugar onde foram criadas. Por isso são ideais para callbacks dentro de métodos e inadequadas como métodos de objetos que precisam do próprio this. Quando um método é passado como callback, por exemplo para um evento, use uma arrow function ou bind para não perder o objeto.

Protótipos e classes

JavaScript implementa herança por delegação: todo objeto tem um elo interno, o [[Prototype]], para outro objeto. Quando você lê uma propriedade que o objeto não tem, o motor procura no protótipo, depois no protótipo do protótipo, até chegar a null.

Diagrama da cadeia de protótipos de um objeto criado a partir de uma classe
A cadeia de protótipos: o objeto pergunta ao protótipo quando não tem a propriedade

A sintaxe de class, de 2015, é uma forma mais clara de escrever exatamente isso. Os métodos de uma classe ficam no protótipo, compartilhados por todas as instâncias, e não copiados em cada objeto:

class Pedido {
  #desconto = 0;                       // campo privado: só acessível dentro da classe

  constructor(valor) { this.valor = valor; }
  total() { return this.valor - this.#desconto; }
  aplicarDesconto(v) { this.#desconto = v; }
}

const p = new Pedido(100);
Object.getPrototypeOf(p) === Pedido.prototype; // true
p.hasOwnProperty('total');                     // false: total mora no protótipo

Os campos com # são privados de verdade, garantidos pela linguagem, e não apenas uma convenção.

Funções, coleções e iteração

Funções como valores

Em JavaScript, uma função é um valor como outro qualquer: pode ser guardada numa variável, passada como argumento e devolvida por outra função. Funções que recebem ou devolvem funções são chamadas de funções de ordem superior, e são elas que dão ao código moderno o seu estilo declarativo. Em vez de um laço com índice, variável acumuladora e condições, descreve-se o que se quer:

const pedidos = [
  { id: 1, status: 'pago', total: 120 },
  { id: 2, status: 'aberto', total: 80 },
  { id: 3, status: 'pago', total: 45 },
];

const pagos = pedidos.filter((p) => p.status === 'pago');            // escolher
const totais = pagos.map((p) => p.total);                             // transformar
const faturado = totais.reduce((soma, t) => soma + t, 0);             // juntar: 165
const temGrande = pedidos.some((p) => p.total > 100);                 // existe algum? true
const primeiroAberto = pedidos.find((p) => p.status === 'aberto');   // o primeiro que atende
const porStatus = Object.groupBy(pedidos, (p) => p.status);           // { pago: [...], aberto: [...] }

O cuidado é não encadear transformações em arrays enormes sem necessidade, porque cada map e cada filter criam um array novo. Para listas de milhares de itens isso é irrelevante; para milhões, um laço simples ou os métodos de iteradores, que processam um item de cada vez, economizam memória.

Map, Set e objetos simples

Por muito tempo, objetos simples foram usados como dicionários. Funciona, mas com limitações: as chaves viram texto, existe o risco de colidir com propriedades herdadas como toString, e não há um jeito direto de saber o tamanho. O **Map** é feito para isso: aceita qualquer valor como chave, mantém a ordem de inserção, tem size e é otimizado para inclusões e remoções frequentes. O **Set** guarda valores sem repetição, o que torna trivial remover duplicados ([...new Set(lista)]) e, desde o ES2025, calcular uniões e interseções.

A regra prática: objeto simples para registros com campos conhecidos (um pedido, um cliente); Map para coleções com chaves variáveis (um cache por código de cliente, uma contagem por produto); Set para "já vi este item?".

Iteradores e geradores

O for...of, o espalhamento (...) e a desestruturação funcionam com qualquer objeto iterável, isto é, que saiba entregar os seus itens um a um. Arrays, textos, Map e Set já são iteráveis, e qualquer objeto pode ser.

O jeito mais simples de criar uma sequência é um gerador, uma função marcada com * que entrega valores com yield e pausa entre um e outro. Nada é calculado antes da hora:

function* paginas(total, porPagina) {
  for (let inicio = 0; inicio < total; inicio += porPagina) {
    yield { inicio, fim: Math.min(inicio + porPagina, total) };
  }
}

for (const p of paginas(95, 30)) console.log(p);
// { inicio: 0, fim: 30 } ... { inicio: 90, fim: 95 }

Os geradores assíncronos (async function*) combinam as duas ideias: produzem valores que chegam com o tempo, consumidos com for await...of. São a ferramenta natural para dados que chegam aos pedaços, como as linhas de um arquivo grande, as páginas de uma API ou o texto de uma resposta de IA sendo gerado em tempo real.

Assíncrono de verdade: o event loop

Se o JavaScript tem uma só thread, como uma página continua respondendo enquanto espera uma requisição de rede, ou como um servidor Node atende milhares de conexões? A resposta é que o JavaScript não espera. Tudo o que demora (temporizadores, rede, disco) é entregue ao ambiente, e o código segue em frente. Quando a espera termina, o ambiente coloca um callback numa fila, e o event loop o executa quando a pilha de chamadas estiver vazia.

Diagrama do event loop com a pilha de chamadas, as APIs do ambiente, a fila de microtarefas e a fila de tarefas
O event loop: a pilha executa, o ambiente espera, e as filas devolvem o trabalho na ordem certa

Há duas filas, com prioridades diferentes. A fila de tarefas recebe callbacks de temporizadores, eventos e entrada e saída. A fila de microtarefas recebe as continuações de Promises e o que vem depois de um await. A regra do laço é: executar o código síncrono até a pilha esvaziar; esvaziar todas as microtarefas; pegar uma tarefa da fila; repetir. No navegador, a tela pode ser redesenhada entre uma tarefa e outra.

Com isso, a ordem do exemplo clássico deixa de ser mistério:

console.log('1: síncrono');
setTimeout(() => console.log('4: tarefa'), 0);
Promise.resolve().then(() => console.log('3: microtarefa'));
console.log('2: síncrono');
// 1: síncrono
// 2: síncrono
// 3: microtarefa
// 4: tarefa

O setTimeout com zero milissegundos não executa "agora": executa na próxima tarefa, depois de todo o código síncrono e de todas as microtarefas.

Um exemplo com await, passo a passo

O await também segue essas regras: ele pausa a função e agenda o restante dela como uma microtarefa. Um exemplo um pouco mais difícil:

async function carregar() {
  console.log('A1');
  await null;
  console.log('A2');
}

console.log('S1');
setTimeout(() => console.log('T1'));
carregar();
Promise.resolve().then(() => console.log('P1'));
console.log('S2');
PassoO que aconteceSaída
1O script imprime S1S1
2O setTimeout entrega o callback ao ambiente, que o põe na fila de tarefas
3carregar() começa a rodar de forma síncrona e imprime A1A1
4O await pausa a função e agenda o resto dela como microtarefa
5O .then agenda P1 como segunda microtarefa
6O script imprime S2 e a pilha esvaziaS2
7O laço esvazia as microtarefas, na ordem: o resto de carregar e o P1A2, P1
8Só então pega a tarefa do temporizadorT1

A ordem final é S1, A1, S2, A2, P1, T1. O ponto que mais surpreende é o passo 3: uma função async não é "assíncrona desde o início"; ela roda normalmente até o primeiro await.

As fases do event loop no Node

No Node, o laço é implementado pela libuv e passa por fases, cada uma com a sua fila:

FaseO que roda
timerscallbacks de setTimeout e setInterval cujo prazo venceu
callbacks pendentesalguns retornos de operações do sistema adiados da volta anterior
pollespera por entrada e saída e roda os callbacks de rede e disco
checkcallbacks de setImmediate
closeeventos de fechamento, como o de um socket

Entre um callback e outro, em qualquer fase, o Node esvazia primeiro a fila do process.nextTick e depois a das microtarefas de Promise. Por isso um setImmediate agendado dentro de um callback de entrada e saída roda sempre antes de um setTimeout de zero: a fase de check vem logo depois da de poll, e a de timers só na volta seguinte.

Cancelar e limitar o tempo

Uma requisição que nunca responde é pior do que uma que falha: ela segura memória, conexões e, às vezes, a paciência de quem espera. A plataforma oferece um mecanismo padrão de cancelamento, o **AbortController**, aceito pelo fetch, por várias APIs do Node e por bibliotecas:

const controle = new AbortController();
botaoCancelar.onclick = () => controle.abort();

const resposta = await fetch('/api/relatorio', {
  // cancela se a pessoa clicar ou se passar de 10 segundos, o que vier primeiro
  signal: AbortSignal.any([controle.signal, AbortSignal.timeout(10_000)]),
});

AbortSignal.timeout cria um sinal que dispara sozinho depois do prazo, e AbortSignal.any combina vários sinais num só. Quem recebe o sinal deve repassá-lo a tudo o que chamar por baixo, para que um cancelamento lá em cima pare o trabalho inteiro, e não só a primeira etapa.

Streams: dados que chegam aos pedaços

Nem toda resposta deve ser esperada inteira. Um arquivo de 2 GB, um relatório gerado linha a linha ou o texto de um assistente de IA sendo escrito palavra por palavra chegam aos poucos, e processá-los conforme chegam economiza memória e mostra resultado mais cedo. As Web Streams (ReadableStream, WritableStream, TransformStream) são o padrão para isso, e hoje existem no navegador, no Node, no Deno e no Bun. O corpo de uma resposta do fetch é um ReadableStream:

const resposta = await fetch('/api/exportacao');
const leitor = resposta.body.getReader();
const decodificador = new TextDecoder();
for (;;) {
  const { done, value } = await leitor.read();
  if (done) break;
  // stream: true evita quebrar um caractere acentuado dividido entre dois pedaços
  mostrar(decodificador.decode(value, { stream: true }));
}

Um detalhe que só aparece em produção: os pedaços da rede não respeitam linhas nem caracteres. Um "ç" pode chegar com metade dos bytes num pedaço e metade no outro, e uma linha de dados pode ser cortada no meio. Por isso o decodificador recebe stream: true, e quem lê linhas guarda o resto num acumulador até a linha se completar. Outro conceito importante é a contrapressão (backpressure): quando quem consome é mais lento que quem produz, a stream sinaliza para a origem diminuir o ritmo, em vez de acumular tudo na memória. No Node, a função pipeline, do módulo node:stream, liga as etapas cuidando disso e dos erros.

De callbacks a async/await

O código assíncrono passou por três gerações. Primeiro vieram os callbacks, funções passadas para serem chamadas no fim da operação, que em fluxos longos viravam pirâmides difíceis de ler e de tratar erros. Depois vieram as Promises, objetos que representam um resultado futuro, encadeáveis com .then e .catch. Por fim, async/await, de 2017, permitiu escrever código assíncrono que se lê de cima para baixo:

async function carregarPainel(clienteId) {
  try {
    // as duas requisições correm em paralelo; esperamos as duas juntas
    const [cliente, pedidos] = await Promise.all([
      fetch(`/api/clientes/${clienteId}`).then((r) => r.json()),
      fetch(`/api/clientes/${clienteId}/pedidos`).then((r) => r.json()),
    ]);
    return { cliente, pedidos };
  } catch (erro) {
    console.error('Falha ao carregar o painel', erro);
    throw erro;
  }
}

Algumas regras evitam a maior parte dos problemas:

  • **Não esqueça o await.** Sem ele, você recebe uma Promise pendente, e um erro dentro dela pode passar em silêncio.
  • Paralelize o que é independente. Dois await seguidos esperam um depois do outro. Promise.all espera os dois juntos; Promise.allSettled espera todos sem falhar no primeiro erro.
  • **Cuidado com forEach e async.** O forEach não espera Promises. Use for...of com await para executar em sequência ou Promise.all com map para executar em paralelo.
  • Trate a rejeição. Toda Promise deve ter um catch em algum ponto da cadeia. No Node, uma rejeição não tratada encerra o processo.

Quando o event loop trava

O modelo de uma thread tem um limite claro: código síncrono pesado bloqueia tudo. Um laço que processa um arquivo enorme, um cálculo longo ou uma expressão regular mal escrita congelam a página ou deixam o servidor sem responder a nenhuma requisição enquanto rodam. Microtarefas que geram novas microtarefas sem fim têm o mesmo efeito, porque o laço só segue depois de esvaziar essa fila. Para trabalho pesado de CPU, use Web Workers no navegador e worker threads no Node, que rodam em outra thread e conversam por mensagens.

No Node, vale conhecer duas particularidades: process.nextTick agenda um callback para antes até das microtarefas de Promise, e setImmediate agenda para a fase seguinte do laço, depois da entrada e saída. Na dúvida, prefira Promises e queueMicrotask, que funcionam igual nos dois ambientes.

Erros e exceções

JavaScript permite lançar qualquer valor com throw, inclusive um texto, mas só objetos Error carregam a pilha de chamadas (stack trace), que mostra onde o problema nasceu. Lance sempre um Error ou uma classe derivada dele. A linguagem já traz alguns tipos: TypeError (usar um valor do jeito errado, como chamar undefined como função), ReferenceError (variável que não existe), RangeError (valor fora do intervalo aceito) e SyntaxError (código ou JSON inválido).

Para regras de negócio, vale criar classes próprias. Elas permitem que quem trata o erro decida pelo tipo, e não comparando mensagens:

class EstoqueInsuficiente extends Error {
  constructor(produto, pedido, disponivel, opcoes) {
    super(`Estoque insuficiente de ${produto}: pedido ${pedido}, disponível ${disponivel}`, opcoes);
    this.name = 'EstoqueInsuficiente';
  }
}

try {
  await reservar(item);
} catch (erro) {
  if (erro instanceof EstoqueInsuficiente) avisarComprador(erro.message);
  else throw new Error('Falha ao reservar o item', { cause: erro }); // preserva a origem
}

A opção cause, de 2022, encadeia erros sem perder o original: o log mostra a falha de alto nível ("falha ao reservar") e, logo abaixo, a de baixo nível ("conexão recusada pelo banco"). Três hábitos completam o quadro. Só capture um erro onde você sabe o que fazer com ele; capturar e ignorar esconde defeitos. Use finally para liberar recursos (fechar um arquivo, esconder um indicador de carregamento) aconteça o que acontecer. E mostre ao usuário uma mensagem compreensível, deixando os detalhes técnicos para o log.

Nos ambientes, existem redes de segurança para o que escapa: o evento unhandledrejection no navegador e o process.on('unhandledRejection') no Node permitem registrar rejeições que ninguém tratou. Elas servem para registrar e investigar, não para substituir o tratamento no lugar certo.

Módulos: CommonJS e ES Modules

Por muito tempo, JavaScript não tinha módulos na linguagem. Os scripts compartilhavam o escopo global, e cada comunidade inventou a sua solução. A que mais se difundiu foi o CommonJS, adotado pelo Node em 2009. Em 2015, a linguagem ganhou o seu sistema oficial, os ES Modules.

Comparação entre CommonJS e ES Modules com exemplos de código
Dois sistemas de módulos: CommonJS, do Node, e ES Modules, o padrão da linguagem

A diferença não é só de sintaxe. No CommonJS, require é uma função comum, executada na hora e de forma síncrona, que pode aparecer em qualquer lugar do código. Nos ES Modules, import e export formam uma estrutura estática, conhecida antes de o código rodar. Isso permite que ferramentas eliminem código não usado (tree shaking), que o carregamento seja assíncrono e que um módulo use await no nível superior. Os ES Modules funcionam igual no navegador e no Node.

No Node, o tipo de módulo é escolhido pela extensão (.mjs para ES Modules, .cjs para CommonJS) ou pelo campo "type": "module" no package.json. Para código novo, a recomendação é usar ES Modules.

Importação dinâmica e divisão do código

Além do import estático, existe o import() como função, que carrega um módulo sob demanda e devolve uma Promise. No navegador, isso permite dividir o código (code splitting): a tela de relatórios, com a sua biblioteca de gráficos, só é baixada quando alguém abre os relatórios. Ferramentas como o Vite fazem essa divisão automaticamente a partir de cada import(), gerando arquivos separados que o navegador baixa quando precisa.

A convivência entre os dois sistemas

A transição de CommonJS para ES Modules durou anos e ainda deixa marcas. Um módulo ES pode importar um módulo CommonJS sem problemas. O caminho contrário, um require de um módulo ES, só passou a funcionar sem opções especiais nas versões mais recentes do Node (a partir da 22.12, com suporte também na linha 20, desde a 20.19), e ainda tem uma restrição: o módulo importado não pode usar await no topo. Pacotes publicados no npm usam o campo exports do package.json para declarar o que oferecem a cada sistema e quais arquivos podem ser importados de fora, o que também impede que alguém dependa de arquivos internos do pacote.

JavaScript moderno em poucos exemplos

As edições anuais trouxeram recursos que deixam o código mais curto e mais seguro. Alguns dos mais úteis no dia a dia:

const pedido = { id: 7, cliente: { nome: 'Ana' }, itens: [{ preco: 10 }, { preco: 25 }] };

const { id, cliente } = pedido;                         // desestruturação
const copia = { ...pedido, status: 'pago' };           // cópia rasa com espalhamento
const cidade = pedido.cliente?.endereco?.cidade;       // encadeamento opcional: undefined, sem erro
const frete = pedido.frete ?? 15;                      // ?? só troca null e undefined (0 continua 0)
const total = pedido.itens.reduce((s, i) => s + i.preco, 0);
const caros = pedido.itens.filter((i) => i.preco > 20);
const ultimo = pedido.itens.at(-1);                    // índice negativo conta do fim
const ordenados = pedido.itens.toSorted((a, b) => a.preco - b.preco); // não altera o original
const independente = structuredClone(pedido);          // cópia profunda
const mensagem = `Pedido ${id} de ${cliente.nome}: R$ ${total}`;

O operador ?? merece destaque. Diferente do ||, ele não troca valores como 0, '' e false, que muitas vezes são válidos. Um frete zero continua zero.

TypeScript: JavaScript com tipos

O TypeScript, criado pela Microsoft em 2012, é JavaScript com um sistema de tipos opcional por cima. Você descreve o formato dos dados, e o compilador aponta erros antes de o código rodar: uma propriedade que não existe, um argumento do tipo errado, um caso esquecido.

interface Pedido {
  id: number;
  status: 'aberto' | 'pago' | 'cancelado';
  frete?: number;
}

function podeCancelar(p: Pedido): boolean {
  return p.status === 'aberto';
}

podeCancelar({ id: 7, status: 'pendente' });
// erro: "pendente" não é um status válido, detectado antes de rodar

O ponto fundamental é que os tipos existem só antes da execução. O compilador verifica e depois remove as anotações, gerando JavaScript comum. Por isso, dados que chegam de fora (uma requisição, um arquivo, uma API) precisam ser validados durante a execução, com bibliotecas de validação como o Zod; o tipo declarado não garante nada sobre o que realmente chegou. Versões recentes do Node já executam arquivos TypeScript diretamente, removendo as anotações na hora, mas a verificação de tipos continua sendo feita pelo compilador.

Em projetos que vão crescer ou que têm mais de uma pessoa, o TypeScript costuma se pagar rapidamente: o editor passa a completar e navegar pelo código com precisão, e refatorações grandes ficam seguras.

Os recursos de tipos que mais ajudam

O sistema de tipos do TypeScript é estrutural: dois tipos com o mesmo formato são compatíveis, tenham o nome que tiverem, o que combina com o jeito como JavaScript trata objetos. Alguns recursos rendem mais do que outros no dia a dia:

  • Uniões discriminadas. Um campo comum diz qual variante o objeto é, e o compilador estreita o tipo em cada ramo de um if ou switch. Se um caso novo for criado e esquecido em algum switch, o compilador aponta.
  • **unknown em vez de any.** O any desliga a verificação; o unknown obriga a conferir o tipo antes de usar. Dados que chegam de fora devem entrar como unknown e sair da validação com o tipo certo.
  • Genéricos. Permitem escrever uma função que funciona com qualquer tipo sem perder a informação: primeiro<T>(lista: T[]): T | undefined devolve um Pedido quando recebe pedidos.
  • **satisfies.** Confere se um valor atende a um tipo sem apagar os detalhes do valor, útil em objetos de configuração.
  • Modo estrito. A opção strict liga um conjunto de verificações, sendo a mais importante a de nulos: o compilador obriga a tratar o caso em que um valor pode ser null ou undefined, a origem de boa parte dos erros em produção.

A validação na fronteira fecha o ciclo. Com o Zod, o mesmo esquema valida os dados durante a execução e gera o tipo, de modo que os dois nunca divergem:

import { z } from 'zod';

const NovoPedido = z.object({
  clienteId: z.number().int().positive(),
  itens: z.array(z.object({ produtoId: z.number(), quantidade: z.number().min(1) })).min(1),
  observacao: z.string().max(500).optional(),
});
type NovoPedido = z.infer<typeof NovoPedido>;   // o tipo sai do esquema

const resultado = NovoPedido.safeParse(corpoDaRequisicao); // corpoDaRequisicao: unknown
if (!resultado.success) responderErro(400, resultado.error.issues);
else criarPedido(resultado.data);                // aqui, resultado.data é NovoPedido

O ecossistema: npm, frameworks e ferramentas

O npm é o gerenciador de pacotes do Node e o maior registro de pacotes de software do mundo. Cada projeto declara as dependências no package.json, com faixas de versão no padrão de versionamento semântico, e o package-lock.json registra as versões exatas instaladas. Em servidores de integração contínua, npm ci instala exatamente o que está no lockfile, o que torna o build reproduzível.

Em volta da linguagem existe um ecossistema enorme:

  • Interfaces: React, Vue, Angular e Svelte, entre outros.
  • Servidores: Express, Fastify e NestJS, este último com módulos, injeção de dependência e TypeScript como padrão.
  • Build e desenvolvimento: Vite, esbuild, Rollup e webpack, que empacotam, minificam e servem o código.
  • Qualidade: ESLint para análise estática, Prettier para formatação, Jest e Vitest para testes, Playwright para testes no navegador.

A abundância tem um custo: cada dependência é código de terceiros rodando com os mesmos privilégios que o seu. Instalar com lockfile, revisar o que entra, preferir pacotes mantidos e usar os recursos nativos quando eles existem (hoje fetch, structuredClone, crypto.randomUUID e muito mais já vêm prontos) reduz o risco de ataques na cadeia de suprimentos.

Segurança no JavaScript

Os riscos mais comuns têm defesas conhecidas.

Injeção de scripts (XSS). Acontece quando um texto vindo do usuário é inserido na página como HTML e o navegador executa o que estiver nele. A defesa principal é nunca montar HTML com dados externos: usar textContent, ou a renderização dos frameworks, que escapa o conteúdo. A segunda camada é a política de segurança de conteúdo (CSP), um cabeçalho HTTP que diz ao navegador de onde scripts, estilos e imagens podem vir. Com script-src 'self', mesmo que um atacante consiga injetar uma tag de script, o navegador se recusa a executar código inline ou de outro domínio.

Poluição de protótipo. Funções que mesclam objetos de forma recursiva, ao receber um JSON como {"__proto__": {"admin": true}}, podem acabar alterando o Object.prototype, e de repente todo objeto do processo "tem" a propriedade admin. A defesa é validar a entrada com um esquema, recusar chaves como __proto__, constructor e prototype ao mesclar, e usar Map ou Object.create(null) para dicionários com chaves vindas de fora.

Expressões regulares catastróficas (ReDoS). Algumas expressões, como /^(a+)+$/, levam um tempo que cresce exponencialmente com certas entradas. Como o event loop tem uma thread só, uma única requisição maliciosa pode travar o servidor inteiro por segundos ou minutos. Evite quantificadores aninhados, limite o tamanho do texto antes de testar e prefira validações simples.

Execução de texto como código. eval, new Function e setTimeout com texto executam qualquer coisa que chegue até eles. Não há bom motivo para usá-los com dados externos, e a CSP pode bloqueá-los por completo.

Segredos no navegador. Todo código enviado ao navegador é público, mesmo minificado. Chaves de API, senhas e regras que precisam ser confiáveis ficam no servidor. Ferramentas como o Vite só expõem ao código do navegador as variáveis de ambiente com um prefixo explícito (VITE_), justamente para evitar vazamentos acidentais.

Cadeia de suprimentos. Pacotes do npm podem rodar scripts durante a instalação, e já houve casos de pacotes legítimos sequestrados e de pacotes falsos com nomes parecidos com os verdadeiros. Lockfile versionado, npm ci, revisão das dependências novas, npm audit periódico e poucas dependências diretas reduzem a exposição.

Desempenho na prática

O primeiro passo é sempre medir. No navegador, a aba Performance das ferramentas do desenvolvedor mostra o que a thread principal fez em cada milissegundo, e as métricas do Google para a experiência real (as Core Web Vitals) apontam o que o usuário sente: quanto demora para o conteúdo principal aparecer, quanto a página pula enquanto carrega e quanto demora para responder a um clique. No Node, performance.now() mede trechos de código e a opção --cpu-prof grava um perfil de CPU que as mesmas ferramentas abrem.

Os ganhos mais comuns raramente vêm de micro-otimizações, e sim de decisões maiores:

  • Enviar menos JavaScript. Cada quilobyte precisa ser baixado, analisado e compilado, e em celulares modestos isso pesa. Dividir o código por tela, remover dependências grandes usadas para pouco e não enviar ao navegador o que pode ser feito no servidor.
  • Renderizar no servidor quando faz sentido. Páginas de conteúdo, como as de um blog ou de um site institucional, podem chegar prontas em HTML e quase não precisar de JavaScript. Aplicações muito interativas, como um painel de gestão, se beneficiam de rodar no navegador.
  • Não fazer trabalho repetido. Uma busca que consulta a API a cada tecla deve esperar a pessoa parar de digitar por alguns milissegundos (a técnica chamada debounce). Resultados caros podem ficar em cache.
  • Não bloquear a thread. No servidor, nada de leitura de arquivo síncrona (readFileSync) no caminho de uma requisição; no navegador, listas com milhares de linhas devem renderizar só o que está visível.

Testes

Testes automatizados são especialmente valiosos numa linguagem dinâmica, em que muitos erros só aparecem quando o código roda. O Node traz um executor de testes embutido, o node:test, que dispensa dependências para projetos simples:

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { somaEmCentavos } from './valores.js';

test('soma valores sem erro de ponto flutuante', () => {
  assert.equal(somaEmCentavos([0.1, 0.2]), 0.3);
});

Para projetos maiores, o Jest e o Vitest oferecem simulação de módulos (mocks), relatórios de cobertura e modo de observação; o Vitest compartilha a configuração do Vite, o que simplifica projetos que já o usam. Para fluxos que dependem de um navegador real, como entrar no sistema e emitir um pedido, o Playwright controla Chrome, Firefox e WebKit. Uma boa distribuição de esforço é ter muitos testes rápidos das regras de negócio (cálculos, conversões, datas, permissões), alguns testes de integração das rotas da API e poucos testes de ponta a ponta dos fluxos mais importantes.

Erros comuns

  • **Usar ==** e ser surpreendido pela coerção. Use ===.
  • Calcular dinheiro com ponto flutuante. Trabalhe com centavos inteiros e formate na exibição.
  • **Esquecer o await** ou usar async dentro de forEach esperando que ele aguarde.
  • **Perder o this** ao passar um método como callback. Use arrow function ou bind.
  • Mutar objetos e arrays compartilhados, como o estado de um componente React. Crie cópias com espalhamento, map, filter ou toSorted.
  • Bloquear o event loop com processamento pesado no servidor. Use workers ou mova o trabalho para uma fila.
  • Confiar em dados externos porque "o tipo diz que é assim". Valide na fronteira do sistema.
  • Montar HTML com dados do usuário usando innerHTML. Isso abre a porta para injeção de scripts (XSS). Use textContent ou a renderização do framework, que escapa o conteúdo.
  • Ignorar rejeições de Promise, que no Node derrubam o processo e no navegador somem no console.

Boas práticas

  • const por padrão, let quando necessário, nunca var.
  • Funções pequenas, com um propósito, e dados tratados de forma consistente.
  • TypeScript em projetos que vão crescer, com o modo estrito ligado.
  • ES Modules em código novo.
  • Lint e formatação automáticos, rodando no editor e na integração contínua.
  • Testes para regras de negócio, especialmente cálculos, datas e conversões.
  • Lockfile versionado e npm ci no build.
  • Tratamento de erros em toda fronteira assíncrona, com logs que ajudem a investigar.

Como usamos JavaScript e TypeScript na Gothor

Na Gothor, o mesmo TypeScript atravessa o navegador e o servidor, o que permite que a equipe trabalhe nas duas pontas com a mesma linguagem, as mesmas ferramentas e as mesmas convenções.

Diagrama do caminho do TypeScript no painel com React e Vite e nas APIs com NestJS e Node.js
O mesmo TypeScript no painel e nas APIs, verificado a cada push

No navegador. O painel do CRM é feito com React 19 e Vite, em TypeScript. O site institucional também usa React 19, com as páginas pré-renderizadas para carregar rápido e serem bem indexadas, e este blog renderiza as páginas no servidor com React 19 e Express.

No servidor. As APIs do CRM, das tarefas e do juniper-motor, o serviço de IA da Juniper, são feitas em NestJS, com módulos e injeção de dependência, e o que chega nas rotas é validado com esquemas do Zod antes de chegar à regra de negócio. O acesso ao PostgreSQL é feito pelo Prisma, que gera os tipos a partir do esquema do banco: se uma coluna muda, o compilador aponta cada lugar do código que precisa mudar junto.

Na integração contínua. A cada push, os workflows do CRM no GitHub Actions instalam as dependências com npm ci, conferem os tipos com tsc --noEmit, rodam o ESLint e os testes com Jest e fazem o build, usando o Node 24. Um erro de tipo no painel ou um teste quebrado na API aparece antes de chegar à main.

Respostas em tempo real. Quando a Juniper responde no app, o texto aparece enquanto é gerado. O juniper-motor envia a resposta em Server-Sent Events, produzida por geradores assíncronos (async function*), e um AbortController ligado à conexão cancela o pedido ao modelo de IA se o app fechar, para não gastar processamento à toa. As chamadas do motor ao CRM e à memória da Juniper combinam o sinal de quem pediu com um AbortSignal.timeout, por meio do AbortSignal.any.

Dinheiro em centavos. No CRM, a tarifa de boletos do GoPay é calculada em centavos inteiros e arredondada uma vez por cliente, para que a soma do mês não acumule erro de ponto flutuante, e um teste confere o valor exato.

Scripts sem dependências. Ferramentas internas são scripts Node em ES Modules que usam o que a plataforma já oferece. O script que envia os posts deste blog para revisão no CRM usa o fetch e o FormData nativos, e as imagens deste artigo foram geradas por um script que só usa módulos do próprio Node.

Segurança por construção. O HTML deste post é gerado por um conversor de Markdown escrito em TypeScript pela própria equipe. Ele escapa todo o texto, aceita links só para endereços seguros e imagens só da biblioteca do blog, e não aceita HTML cru. É uma escolha deliberada: menos uma dependência e nenhuma brecha para injeção de scripts. As páginas do blog chegam prontas do servidor, sem React no navegador, com uma política de segurança de conteúdo que só aceita scripts do próprio site.

Resumo

  • JavaScript é dinâmico, de tipagem fraca e multiparadigma; ECMAScript é o padrão, e o TC39 publica uma edição por ano.
  • A linguagem é a mesma em todo lugar; motores (V8, SpiderMonkey, JavaScriptCore) a executam, e ambientes (navegador, Node.js, Deno, Bun) fornecem o resto.
  • Os motores compilam o código em tempo de execução e otimizam o que é previsível; vazamentos de memória são referências esquecidas.
  • Use ===, cuidado com ponto flutuante, prefira const e let, e lembre que this depende de como a função é chamada.
  • Herança é delegação pela cadeia de protótipos; class é uma sintaxe mais clara para isso.
  • O event loop executa o síncrono, esvazia as microtarefas e pega uma tarefa por vez; async/await deixa o código assíncrono legível, e código pesado deve ir para workers.
  • Dinheiro em centavos inteiros, datas em UTC com o fuso só na exibição, textos normalizados antes de comparar.
  • Erros são objetos Error com cause; cancelamento e prazo vêm do AbortController; dados que chegam aos pedaços, das streams.
  • ES Modules são o padrão para código novo, e TypeScript traz tipos que existem só antes da execução; a validação na fronteira fecha o ciclo.
  • XSS, poluição de protótipo, ReDoS e dependências comprometidas têm defesas conhecidas: escapar, validar, CSP e lockfile.

Para ir além

Este post foi escrito com a ajuda da Juniper, a assistente de IA da Gothor, e revisado pela nossa equipe antes de ir ao ar.

Comentários

Ainda não há comentários. Que tal ser o primeiro?

Entre na sua conta para comentar. É grátis.

Receba os próximos posts por e-mail

Um e-mail a cada post novo, sem spam. Você cancela quando quiser.

Para ampliar, use a roda do mouse, os botões, as teclas + e − ou o gesto de pinça; toque duas vezes para ampliar ou voltar. Com a imagem ampliada, arraste ou use as setas para mover. Esc fecha.