Modern JavaScript Best Practices: Writing Cleaner, More Maintainable Code
JavaScript is the world’s most widely used programming language, powering the vast majority of modern web applications. With such widespread influence, it’s crucial to follow best practices that keep your JavaScript code clean, maintainable, and high-performing. In this guide, we’ll explore current best practices for writing modern JavaScript, along with why each practice matters.
1. Follow Project-Specific Rules First
Key Point: The rules defined in your project trump any external advice, including what you read here!
Before adopting a new convention or best practice, ensure it aligns with your team’s existing codebase guidelines. Everyone on your team should be on board with new standards to maintain consistency.
2. Use the Latest JavaScript Features Responsibly
Why It Matters:
- JavaScript has been evolving since 1995. Not all online advice is up to date.
- Always confirm the stage of new language features; Stage 3 or beyond is typically safe to use.
Recommendation:
- Keep track of ECMAScript releases and TC39 proposals to stay informed about the latest and most reliable features.
3. Prefer let
and const
Over var
// Using let
for (let j = 1; j < 5; j++) {
console.log(j);
}
console.log(j); // ReferenceError: j is not defined
Why It Matters:
let
andconst
provide block scoping, preventing many of the pitfalls associated withvar
.- They reduce accidental variable leaks and make code behavior more predictable.
4. Use Classes Instead of Function Prototypes
Old Prototype Style:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
Modern Class Syntax:
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
Why It Matters:
- Class syntax is more intuitive and cleaner than the prototype approach.
- Built-in language features like private fields (
#name
) provide true data encapsulation.
5. Leverage Private Class Fields (#
)
Old “Private” Convention:
class Person {
constructor(name) {
this._name = name; // Not truly private
}
getName() {
return this._name;
}
}
Modern Private Fields:
class Person {
#name;
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
Why It Matters:
#
fields enforce true privacy, preventing unintended external access.- Improves encapsulation and code maintainability.
6. Adopt Arrow Functions
// More concise
const numbers = [1, 2];
numbers.map(num => num * 2);
// Instead of
numbers.map(function (num) {
return num * 2;
});
Why It Matters:
- Arrow functions provide a compact syntax and automatically bind
this
to the surrounding scope. - Ideal for inline callbacks and class methods to avoid common
this
binding issues.
7. Use Nullish Coalescing (??
) Instead of ||
// With ||, 0 is treated as falsy
const value = 0;
const result = value || 10; // result is 10, which may be unintended
// With ??
const resultCorrect = value ?? 10; // resultCorrect is 0 (as expected)
Why It Matters:
||
can override valid falsy values like0
,false
, or""
.??
checks specifically fornull
orundefined
, making your default values more reliable.
8. Apply Optional Chaining (?.
)
const product = {};
// Without optional chaining
const tax = (product.price && product.price.tax) ?? undefined;
// With optional chaining
const taxSafe = product?.price?.tax;
Why It Matters:
- Simplifies code when accessing nested properties.
- Prevents runtime errors by returning
undefined
if an intermediate property is missing.
9. Prefer async/await
Over Promise Chaining
// Older Promise chaining
function fetchData() {
return fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
}
// With async/await
async function fetchData() {
try {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
}
Why It Matters:
async/await
results in cleaner, more readable asynchronous code.- Error handling is straightforward with
try...catch
.
10. Modern Object Iteration: Object.entries()
and Object.values()
const obj = { a: 1, b: 2, c: 3 };
Object.entries(obj).forEach(([key, value]) => {
console.log(key, value);
});
Why It Matters:
- More concise than manual loops or
for...in
. - Simplifies transformations and keeps code more readable.
11. Use Array.isArray()
to Check for Arrays
const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
Why It Matters:
- Reliable across different contexts, unlike
instanceof
. - Eliminates confusion when validating array types in complex or cross-environment applications.
12. Use Map
Instead of Plain Objects for Key-Value Storage
const map = new Map();
const keyObj = { id: 1 };
map.set(keyObj, 'value');
console.log(map.get(keyObj)); // 'value'
Why It Matters:
Map
can use any data type (including objects) as keys.- Preserves insertion order and offers better performance for complex key-value operations.
13. Symbols for “Hidden” Keys
const hiddenKey = Symbol('hidden');
obj[hiddenKey] = 'Secret Value';
Why It Matters:
- Prevents accidental overrides by creating truly unique property keys.
- Symbols are non-enumerable, keeping internal or private data “hidden” from typical object operations.
14. Check the Intl
API Before Adding Libraries
const amount = 123456.78;
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
console.log(formatter.format(amount)); // "$123,456.78"
Why It Matters:
- The built-in
Intl
API handles date, number, and currency formatting for various locales. - Reduces dependencies and potential bundle size compared to third-party libraries.
15. Use Strict Equality (===
) Over Loose Equality (==
)
console.log(0 == ''); // true (loose equality)
console.log(0 === ''); // false (strict equality)
Why It Matters:
- Prevents unexpected behavior due to type coercion.
- Makes code safer and more predictable.
16. Make if
Statement Conditions Explicit
// Implicit (can be misleading)
if (value) { ... }
// Explicit checks
if (value !== 0) { ... }
if (name != null) { ... } // Checks for both null and undefined
Why It Matters:
- JavaScript treats many values (e.g.
0
,false
) as falsy, which can cause surprises. - Explicit conditions clarify your intent and reduce logical errors.
17. Avoid Built-In Number
for High Precision Tasks
console.log(0.1 + 0.2); // 0.30000000000000004
Why It Matters:
- IEEE 754 floating-point arithmetic can introduce rounding errors.
- For financial or sensitive calculations, use libraries like decimal.js or big.js.
18. Handle Large Integers Properly (BigInt & JSON)
// JavaScript can lose precision for numbers > Number.MAX_SAFE_INTEGER
JSON.parse('{"id": 9007199254740999}');
// Might yield incorrect integer
// Use reviver to treat large numbers as strings
JSON.parse('{"id": 9007199254740999}', (key, value, ctx) => {
if (key === 'id') return ctx.source;
return value;
});
Why It Matters:
- Prevents data corruption or precision loss with very large numbers.
BigInt
is not directly JSON-serializable, so use a customreplacer
andreviver
if needed.
19. Document Using JSDoc
/**
* @typedef {Object} User
* @property {string} firstName
* @property {string} [middleName]
* @property {string} lastName
*/
/**
* Prints a user’s full name.
* @param {User} user - The user object.
* @return {string} - The full name.
*/
const printFullUserName = (user) =>
`${user.firstName} ${user.middleName ? user.middleName + ' ' : ''}${user.lastName}`;
Why It Matters:
- Helps developers (including you, later) understand function signatures and data structures.
- Many editors and IDEs provide autocompletion and type-checking based on JSDoc.
20. Write Tests (Node Has a Built-In Test Runner)
// Node.js 20+ built-in test runner example
import { test } from 'node:test';
import { equal } from 'node:assert';
function sum(a, b) {
return a + b;
}
test('sum function', () => {
equal(sum(1, 1), 2);
});
Why It Matters:
- Automated tests reduce the risk of regressions and catch bugs early.
- Modern runtimes (Node, Deno, Bun) include built-in test runners, making it simpler to adopt TDD or BDD.
Final Thoughts
Staying current with JavaScript best practices can feel overwhelming, given how frequently the language evolves. However, by incorporating these tips:
- You’ll write more readable and concise code.
- You’ll avoid common pitfalls like floating-point inaccuracies and hidden
this
binding issues. - You’ll keep your project up-to-date with ECMAScript standards, ensuring long-term maintainability.
Keep an eye on the latest ECMAScript releases and follow TC39 proposals to stay ahead of the curve. By embracing modern best practices, you’re setting yourself up for success in building scalable, robust, and high-quality applications.
Questions, feedback, or additional tips? Share them in the comments below. Happy coding!
What do you think?
Show comments / Leave a comment