Knowing the syntax and writing good code are two different skills. By now you can declare variables, write functions, loop over arrays, and reach into objects — the mechanics are behind you. But code that merely works isn’t the finish line. Real code gets read far more often than it gets written: by teammates, by future-you six months from now, and by whoever has to fix the bug at 2 a.m. The goal of this article is to turn the language you already know into code that’s clean, predictable, and a relief to maintain.
None of what follows is exotic. These are everyday habits that experienced developers apply almost without thinking — and once you internalize them, you will too. We’ll build on everything from the earlier articles, so if a concept feels shaky, the links along the way point back to the foundations.
Use const by default, let when needed, never var
This is the single most impactful habit you can adopt, and it’s the easiest. We covered the three keywords back in the variables and data types article; the practice is simple:
- Reach for
constfirst, every time. - Switch to
letonly when you genuinely need to reassign the variable. - Never use
varin new code.
const taxRate = 0.11; // never changes → const
let runningTotal = 0; // reassigned in a loop → let
for (const item of cart) {
runningTotal += item.price;
}
Why default to const? It’s not about the value being frozen — it’s about intent. When a reader sees const, they know that name will always point at the same thing, so they can stop tracking it in their head. let is a small flag that says “watch this, it changes.” That signal is genuinely useful, and const makes it the default rather than the exception.
const doesn't mean the value is frozen
const stops you from reassigning the variable, not from mutating what it points to. With an object or array, you can still change the contents:
const user = { name: "John Doe" };
user.name = "Jane Doe"; // allowed — we mutated the object
user = {}; // TypeError — we tried to reassignSo const guarantees the binding is stable, not that the data is immutable. If you need a truly read-only object, that’s a separate tool (Object.freeze), not what const does.
var is worth avoiding for a concrete reason: it’s function-scoped and hoisted in ways that quietly cause bugs, while let and const are block-scoped and behave the way you’d expect. There’s a whole article on why the difference matters — for everyday code, just leave var in the past.
Name things clearly
Good names are documentation that never goes out of date. A variable called d tells you nothing; daysUntilExpiry tells you everything. The few extra keystrokes pay for themselves the first time someone reads the code.
// Vague — what is this? what unit?
const t = 86400;
function calc(a, b) { return a * b; }
// Clear — the names explain themselves
const secondsPerDay = 86400;
function calculateTotal(price, quantity) {
return price * quantity;
}
A few conventions that the whole JavaScript world follows, so your code looks familiar to everyone:
- Variables and functions use
camelCase:userName,calculateTotal. - Constants that are true fixed values are sometimes written in
UPPER_SNAKE_CASE:MAX_RETRIES,API_BASE_URL. - Booleans read well with an
is,has, orcanprefix:isActive,hasPermission,canEdit. - Functions start with a verb because they do something:
getUser,sendEmail,formatPrice.
Don’t abbreviate to save space, and don’t be afraid of slightly longer names. cnt saves three characters and costs every future reader a moment of guessing. count is better.
Keep functions small and focused
We dug into functions in the functions article. The best-practice layer on top is simple: a function should do one thing, and its name should tell you what that thing is. When a function starts juggling three unrelated jobs, split it.
// Doing too much: validating, calculating, AND formatting
function processOrder(order) {
if (!order.items.length) throw new Error("Empty order");
let total = 0;
for (const item of order.items) total += item.price * item.qty;
return "Total: Rp " + total.toLocaleString("id-ID");
}
// Split into focused pieces — each is easy to read and test
function validateOrder(order) {
if (!order.items.length) throw new Error("Empty order");
}
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
function formatRupiah(amount) {
return "Rp " + amount.toLocaleString("id-ID");
}
Small functions are easier to name, easier to reuse, and far easier to debug — when something breaks, you know exactly which little piece to look at. A good rule of thumb: if you struggle to name a function because it does several things, that’s the code telling you to break it apart.
Avoid deep nesting with early returns
Deeply nested if blocks are hard to follow because you have to hold every condition in your head at once. The fix is the early return (also called a guard clause): handle the exceptional cases first and bail out, so the main logic isn’t buried under indentation.
// Hard to follow — the real work is four levels deep
function getDiscount(user) {
if (user) {
if (user.isActive) {
if (user.purchases > 10) {
return 0.2;
}
}
}
return 0;
}
// Flat and readable — handle the "no" cases first, then proceed
function getDiscount(user) {
if (!user) return 0;
if (!user.isActive) return 0;
if (user.purchases <= 10) return 0;
return 0.2;
}
The second version reads top to bottom like a checklist. Each guard clause peels off one case and gets out of the way, so by the time you reach the bottom, the happy path stands alone with no indentation hiding it.
Use strict equality (===)
JavaScript has two equality operators, and you should almost always use the strict one. == (loose equality) tries to convert the two values to the same type before comparing, which produces surprising results. === (strict equality) compares value and type, with no conversion — exactly what you usually want.
0 == ""; // true — both coerced to falsy. Surprising!
0 == "0"; // true — string "0" converted to number
"" == "0"; // false — wait, what?
null == undefined; // true
0 === ""; // false — different types, no conversion
"0" === 0; // false — clear and predictable
The loose == rules are a maze of edge cases that even experienced developers can’t fully recite from memory. By using === everywhere, you sidestep the whole problem. The same goes for !== over !=.
A linter catches this for you
You don’t have to remember every rule by hand. A linter like ESLint flags ==, unused variables, undeclared names, and dozens of other mistakes as you type — many editors underline them instantly. Setting one up is the single highest-leverage thing you can do for code quality, because it enforces these habits automatically instead of relying on your memory.
Handle errors instead of ignoring them
Code that assumes everything always works is fragile. Network requests fail, JSON arrives malformed, users type nonsense. The data you work with isn’t always the data you expected, so plan for the unhappy path.
Wrap risky operations in try...catch, and never swallow an error silently:
// Bad — if parsing fails, the program crashes or behaves strangely
const data = JSON.parse(rawInput);
// Good — handle the failure deliberately
try {
const data = JSON.parse(rawInput);
render(data);
} catch (error) {
console.error("Failed to parse input:", error.message);
showFallbackUI();
}
The worst thing you can do is catch an error and do nothing with it — an empty catch {} block hides problems instead of solving them, and turns a clear crash into a silent, baffling bug later. If you catch an error, do something: log it, show the user a message, fall back to a default. Want to go deeper? The dedicated data-handling patterns from earlier in this series pair naturally with solid error handling.
Prefer array methods over manual loops
You learned the classic for loop in the loops article, and it’s still the right tool sometimes. But for the most common jobs — transforming a list, filtering it, or summing it up — the built-in array methods say what you mean far more clearly than a hand-rolled loop with a counter and a temporary array.
const prices = [100, 250, 80, 300];
// Manual loop — works, but noisy
const withTax = [];
for (let i = 0; i < prices.length; i++) {
withTax.push(prices[i] * 1.11);
}
// Declarative — reads like its intent: "map each price to price * 1.11"
const withTax = prices.map((price) => price * 1.11);
// Filter and reduce read just as cleanly
const expensive = prices.filter((price) => price > 150);
const total = prices.reduce((sum, price) => sum + price, 0);
map, filter, and reduce describe what you want rather than how to iterate, which is exactly why they’re easier to read. They also avoid the off-by-one index mistakes that manual loops invite. Keep the plain for loop for the cases that need it — breaking out early, complex stepping — and let the methods handle the everyday transformations.
Don’t repeat yourself
If you find yourself copying and pasting the same few lines, that’s a signal to extract them into a function. Repeated code isn’t just longer — it’s a maintenance trap, because the day you need to fix that logic, you have to remember every place you pasted it.
// Repetition — the same formatting logic, three times
const label1 = "Rp " + price1.toLocaleString("id-ID");
const label2 = "Rp " + price2.toLocaleString("id-ID");
const label3 = "Rp " + price3.toLocaleString("id-ID");
// One source of truth — fix it once, it's fixed everywhere
function formatRupiah(amount) {
return "Rp " + amount.toLocaleString("id-ID");
}
const label1 = formatRupiah(price1);
const label2 = formatRupiah(price2);
const label3 = formatRupiah(price3);
This principle is often called DRY — Don’t Repeat Yourself. Don’t take it to a silly extreme (a tiny bit of duplication is fine, and forcing two barely-related things to share code can be worse than copying), but when the same real logic shows up three times, give it a name and a home.
Write code for humans to read
Computers run whatever you give them, but people have to understand it. Small touches of clarity add up:
- Comment the why, not the what. The code already shows what it does; a good comment explains why it does it that way.
// add 1 to iis noise;// retry once before giving up, the API is flaky on first callis gold. - Use whitespace to group related lines. A blank line between logical steps acts like a paragraph break.
- Be consistent. Pick a style — quote marks, indentation, semicolons — and stick to it across the whole file. A formatter like Prettier does this for you automatically.
// What: redundant, the code already says this
let count = 0; // set count to zero
// Why: explains a decision the code can't
let retries = 0;
const MAX_RETRIES = 3; // 3 because the upstream service times out around the 4th hit
Consistency matters more than any single choice. Whether you use semicolons or not, single or double quotes, two spaces or four — what counts is that the whole codebase agrees, so a reader never has to wonder whether a difference is meaningful.
Let tools enforce the boring stuff
Two tools do the tedious work so you can focus on logic: a formatter (like Prettier) keeps spacing, quotes, and indentation uniform automatically, and a linter (like ESLint) catches likely bugs and bad patterns. Set them up once and they run on every save. When you’re working with structured data like JSON, an online JSON formatter and validator is handy for quickly checking that a payload is well-formed before you debug your code.
Avoid polluting the global scope
When you declare things at the top level without care, they live in the global scope where any other script can read or clobber them. That’s a recipe for name collisions and bugs that are maddening to track down. Thanks to functions and modern modules, the fix is easy: keep variables in the smallest scope that works, and use ES modules so each file has its own private namespace.
// Polluting the global scope — anything can overwrite `total`
total = 0;
function add(n) { total += n; }
// Contained — `total` lives only where it's needed
function sumAll(numbers) {
let total = 0;
for (const n of numbers) total += n;
return total;
}
Declaring variables in the narrowest scope possible — inside the function or block that uses them — keeps each piece of your program independent. Independent pieces don’t step on each other, which is exactly what makes a large codebase manageable.
Putting it together
Here’s the same small task written twice — once carelessly, once with the habits above — so you can see how they compound:
// Before: works, but rough
function p(d) {
var r = [];
for (var i = 0; i < d.length; i++) {
if (d[i].active == true) {
r.push(d[i].name + ": Rp " + d[i].price);
}
}
return r;
}
// After: same result, far clearer
function formatActiveProducts(products) {
return products
.filter((product) => product.isActive)
.map((product) => `${product.name}: Rp ${product.price.toLocaleString("id-ID")}`);
}
The second version uses descriptive names, const-friendly array methods, strict comparison via a clean boolean, and does its one job clearly. It’s shorter and easier to understand — that’s the payoff of good habits. They don’t slow you down; once they’re automatic, they make you faster, because you spend less time deciphering your own past code.
Wrapping up
Best practices aren’t rules handed down to make your life harder — they’re shortcuts to code that’s easier to read, safer to change, and quicker to debug. The ones worth burning into your habits:
- Declare with
constby default,letonly when reassigning, and nevervar— but rememberconststops reassignment, not mutation. - Name things clearly with
camelCase, verb-first functions, andis/hasbooleans — names are free documentation. - Keep functions small and focused on one job, and flatten nesting with early returns.
- Use
===(strict equality) to avoid the surprises of type coercion. - Handle errors with
try...catchand never silently swallow them. - Prefer
map/filter/reduceover manual loops for everyday list work. - Don’t repeat yourself — extract repeated logic into a named function.
- Write for humans: comment the why, stay consistent, and let a formatter and linter enforce the rest.
- Keep scope small and use modules so your pieces stay independent.
You don’t have to perfect all of these overnight. Pick one or two — const by default and === everywhere are great starting points — and let them become second nature before adding the next. Good code is a skill you build one habit at a time, and you already have everything you need to start.