JavaScript lets you do almost anything. The problem is that "almost anything" includes plenty of things that will blow up in your hands in production, three weeks later, at the worst possible time. The good news: most ways of shooting yourself in the foot come from a handful of habits, and all of them can be fixed. Here are the ones that pay off the most.

Retire var: use only const and let
There is no reason to use var in new code. It leaks function scope, gets hoisted to the top, and lets you reassign values by accident. Use const by default and switch to let only when you REALLY need to reassign the variable.
js const taxa = 0.15; let total = 0; for (const item of carrinho) total += item.preco;
Rule of thumb: start everything as const. If the linter complains that you need to reassign it, then change it to let. var is 2014 code.
Always use ===, never ==
== performs type coercion before comparing, and its rules are a small minefield. 0 == '' is true. null == undefined is true. [] == false is true. Nobody has this table memorized, and you shouldn't need to.
js if (valor === 0) { / predictable / }
Always use === and !==. If you need to treat null and undefined the same way, be explicit in that specific case—or handle it with ??.
async/await instead of callback hell
A callback nested inside another callback inside yet another callback is the famous pyramid of doom. async/await makes asynchronous code look synchronous, one line at a time.
js async function carregarPerfil(id) { const user = await buscarUsuario(id); const posts = await buscarPosts(user.id); return { user, posts }; }
Need things to run in parallel? Use Promise.all instead of awaiting them sequentially:
js const [user, config] = await Promise.all([buscarUsuario(id), buscarConfig(id)]);
An unhandled promise is a ticking time bomb
Pretty async/await code won't save you from errors. A promise that rejects without anyone handling it becomes an unhandledRejection—in modern Node, that can bring down the process.
js try { const dados = await buscarDados(); usar(dados); } catch (err) { logger.error('failed to fetch data', err); // decide: fallback, re-throw, or return an error }
Every asynchronous operation that can fail needs a try/catch or a .catch(). "I'll deal with it later" is how production bugs are born.
ESM and immutability: fewer surprises, fewer bugs
Use ESM modules (import/export), not global variables or require scattered throughout the codebase. A module makes it explicit what goes in and what comes out, the bundler can perform tree-shaking, and nothing leaks into the global scope without you noticing.
js // util.js export function slugify(texto) { / ... / } // app.js import { slugify } from './util.js';
Along with that, avoid mutating data unnecessarily. map, filter, and reduce return new arrays instead of changing the original—fewer side effects, fewer phantom bugs.
js const ativos = usuarios.filter(u => u.ativo); const nomes = ativos.map(u => u.nome);
Mutation is not forbidden, but when you need it, make it a conscious decision—not an accident.
this is not your friend
In JavaScript, this depends on HOW a function is called, not where it was defined. Pass a method as a callback and this disappears.
js class Timer { segundos = 0; // arrow preserves the instance's this tick = () => { this.segundos++; }; } setInterval(timer.tick, 1000); // works
Arrow functions do not have their own this—they inherit it from the scope where they were created. That is why they solve 90% of callback-related headaches. Just be careful: DO NOT use an arrow function as a method when you depend on dynamic this (for example, DOM handlers that expect this to be the element).
None of these practices is sophisticated. They are small habits that, taken together, turn "why did this break?" into code that does exactly what it looks like it should do. And predictable code is the only kind you can maintain at 2 a.m.

