JavaScript te deixa fazer quase tudo. O problema é que "quase tudo" inclui um monte de coisa que vai explodir na sua mão em produção, três semanas depois, num horário ruim. A boa notícia: a maioria dos tiros no pé vem de um punhado de hábitos, e todos têm cura. Aqui vão os que mais rendem.

Aposente o var: só const e let
Não existe motivo pra usar var em código novo. Ele vaza escopo de função, sobe pro topo com hoisting e te deixa reatribuir sem querer. Use const por padrão e só troque pra let quando você REALMENTE for reatribuir a variável.
const taxa = 0.15;
let total = 0;
for (const item of carrinho) total += item.preco;
Regra prática: comece tudo como const. Se o linter reclamar que precisa reatribuir, aí vira let. var é código de 2014.
=== sempre, == nunca
O == faz coerção de tipo antes de comparar, e as regras são um pequeno campo minado. 0 == '' é true. null == undefined é true. [] == false é true. Ninguém tem essa tabela na cabeça, e você não devia precisar.
if (valor === 0) { /* previsível */ }
Use === e !== sempre. Se precisar tratar null/undefined juntos, seja explícito nesse caso pontual — ou resolva com ??.
async/await no lugar do inferno de callbacks
Callback aninhado dentro de callback dentro de callback é a famosa pirâmide da desgraça. async/await deixa código assíncrono parecer síncrono, linha após linha.
async function carregarPerfil(id) {
const user = await buscarUsuario(id);
const posts = await buscarPosts(user.id);
return { user, posts };
}
Precisa de coisas em paralelo? Promise.all em vez de await em série:
const [user, config] = await Promise.all([buscarUsuario(id), buscarConfig(id)]);
Promise sem tratamento é bomba-relógio
async/await bonito não te salva de erro. Uma promise que rejeita e ninguém pega vira um unhandledRejection — em Node moderno, isso derruba o processo.
try {
const dados = await buscarDados();
usar(dados);
} catch (err) {
logger.error('falha ao buscar dados', err);
// decida: fallback, re-throw ou responder erro
}
Toda operação assíncrona que pode falhar precisa de um try/catch ou de um .catch(). "Depois eu trato" é como bug de produção nasce.
ESM e imutabilidade: menos surpresa, menos bug
Use módulos ESM (import/export), não variáveis globais nem require espalhado. Módulo deixa explícito o que entra e o que sai, o bundler faz tree-shaking, e nada vaza pro escopo global sem você ver.
// util.js
export function slugify(texto) { /* ... */ }
// app.js
import { slugify } from './util.js';
Junto disso, evite mutar dados sem necessidade. map, filter e reduce retornam novos arrays em vez de mexer no original — menos efeito colateral, menos bug fantasma.
const ativos = usuarios.filter(u => u.ativo);
const nomes = ativos.map(u => u.nome);
Mutação não é proibida, mas quando você precisa dela, que seja uma decisão consciente — não um acidente.
O this não é seu amigo
this em JavaScript depende de COMO a função é chamada, não de onde ela foi definida. Passe um método como callback e o this some.
class Timer {
segundos = 0;
// arrow preserva o this da instância
tick = () => { this.segundos++; };
}
setInterval(timer.tick, 1000); // funciona
Arrow functions não têm this próprio — herdam do escopo onde foram criadas. Por isso resolvem 90% das dores de cabeça com callbacks. Só cuidado: NÃO use arrow como método quando você depende de this dinâmico (por exemplo, handlers do DOM que esperam this = elemento).
Nenhuma dessas práticas é sofisticada. São hábitos pequenos que, somados, transformam "por que isso quebrou?" em código que faz exatamente o que parece que faz. E código previsível é o único tipo que você consegue manter às duas da manhã.

