10 Modern JavaScript Tricks That Instantly Cut Boilerplate Code
Write less, avoid common pitfalls, and make everyday JavaScript cleaner.
Modern JavaScript keeps getting smaller.
Features that once required helper functions, verbose conditionals, or entire utility libraries are now built directly into the language. Yet many developers continue writing code the same way they did years ago.
The result is unnecessary boilerplate, harder-to-read logic, and bugs that modern syntax was designed to prevent.
This article covers ten JavaScript techniques that can dramatically reduce repetitive code while making applications easier to maintain. Each example includes practical advice and the most common mistakes developers still make.
1. Optional Chaining Eliminates Nested Property Checks
Checking whether every nested property exists used to produce long, repetitive conditions.
Before
if (
user &&
user.profile &&
user.profile.address &&
user.profile.address.city
) {
console.log(user.profile.address.city);
}Modern JavaScript
console.log(user?.profile?.address?.city);Optional chaining immediately returns undefined whenever one part of the chain is null or undefined.
The code becomes shorter and far easier to read.
Common mistake
Optional chaining only checks for null and undefined.
Values such as 0, false, and an empty string are perfectly valid and will continue through the chain.
2. Prefer ?? Instead of || for Default Values
One of the most common JavaScript bugs comes from using logical OR to provide defaults.
function setVolume(volume) {
return volume || 50;
}
setVolume(0); // 50 ❌Passing 0 should not reset the value.
Instead, use the nullish coalescing operator.
function setVolume(volume) {
return volume ?? 50;
}
setVolume(0); // 0 ✅
setVolume(null); // 50
setVolume(undefined); // 50Common mistake
?? cannot be mixed directly with || or &&.
This causes a syntax error:
// ❌
value || other ?? fallback;Instead, group expressions explicitly.
(value || other) ?? fallback;3. Learn the Logical Assignment Operators
Modern JavaScript combines assignment and conditional logic into a single operator.
settings.theme ??= "dark";
cache.enabled ||= true;
user.name &&= user.name.trim();Each operator behaves differently.
let count = 0;
count ||= 10;
console.log(count); // 10
count = 0;
count ??= 10;
console.log(count); // 0Quick reference
||=replaces falsy values.??=replaces onlynullorundefined.&&=updates only truthy values.
Choosing the wrong operator often introduces subtle bugs around 0, false, and empty strings.
4. Use Object.prototype.toString.call() for Reliable Type Detection
typeof works well for primitives but quickly falls apart with complex objects.
typeof [];
// "object"
typeof null;
// "object"A more reliable solution is:
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1);
}
getType([]);
// "Array"
getType(new Map());
// "Map"
getType(new Date());
// "Date"
getType(/js/);
// "RegExp"This approach correctly distinguishes nearly every built-in JavaScript object.
When to use typeof
Use typeof for:
string
number
bigint
boolean
symbol
undefined
function
Use Object.prototype.toString.call() when you need precise object types.
5. Always Use Array.isArray()
Checking arrays with instanceof can fail across browser windows or iframes.
value instanceof Array;The safer solution is built into JavaScript.
Array.isArray(value);It also avoids confusing array-like objects.
Array.isArray([]);
// true
Array.isArray({ length: 2 });
// falseWhenever you need to detect arrays, this should be your default choice.
6. ES Modules Already Implement the Singleton Pattern
Many projects still include unnecessary singleton classes.
class Config {
static instance;
static getInstance() {
if (!this.instance) {
this.instance = new Config();
}
return this.instance;
}
}In modern JavaScript, a module is loaded only once.
// config.js
export const config = {
apiUrl: "/api",
timeout: 5000,
};import { config } from "./config.js";Every import receives the same object.
No singleton boilerplate required.
Watch out
Exported objects remain mutable.
If multiple modules modify shared state, unexpected side effects can appear.
7. Replace Large if Statements with a Strategy Object
Long chains of conditions become difficult to maintain.
function calculate(type, a, b) {
if (type === "add") return a + b;
if (type === "subtract") return a - b;
if (type === "multiply") return a * b;
if (type === "divide") return a / b;
throw new Error("Unknown operation");
}A lookup table is cleaner.
const operations = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
};
function calculate(type, a, b) {
const operation = operations[type];
if (!operation) {
throw new Error(`Unknown operation: ${type}`);
}
return operation(a, b);
}Adding new functionality now means adding one new property instead of editing existing logic.
TypeScript tip
type Operation = keyof typeof operations;
function calculate(type: Operation, a: number, b: number) {
return operations[type](a, b);
}This catches invalid operation names before runtime.
8. Build a Tiny Event Emitter
The Observer pattern powers everything from UI events to state management.
A minimal implementation is surprisingly small.
class EventEmitter {
#listeners = new Map();
on(event, listener) {
const list = this.#listeners.get(event) ?? [];
list.push(listener);
this.#listeners.set(event, list);
}
off(event, listener) {
const list = this.#listeners.get(event);
if (!list) return;
this.#listeners.set(
event,
list.filter(fn => fn !== listener)
);
}
emit(event, ...args) {
for (const listener of this.#listeners.get(event) ?? []) {
listener(...args);
}
}
}Usage is simple.
const events = new EventEmitter();
events.on("login", user => {
console.log(`${user.name} signed in`);
});
events.emit("login", {
name: "Alex",
});Common mistake
Always remove listeners when components are destroyed.
Forgotten subscriptions remain one of the most common causes of memory leaks.
9. Proxy Can Create Smarter Read-Only Objects
Object.freeze() freezes everything.
Proxy lets you intercept only the operations you care about.
function readonly(object) {
return new Proxy(object, {
set(_, property) {
throw new Error(`${String(property)} is read-only`);
},
deleteProperty(_, property) {
throw new Error(`${String(property)} cannot be deleted`);
},
});
}
const config = readonly({
api: "/v1",
});
config.api = "/v2";
// ErrorCommon mistake
The original object is still mutable.
const original = {
api: "/v1",
};
const safe = readonly(original);
original.api = "/v2";
// succeedsDeep read-only protection requires recursively wrapping nested objects.
10. Decorate Functions Instead of Editing Them
Adding logging, timing, retries, or authorization directly into business logic quickly creates messy functions.
A higher-order function keeps concerns separate.
function withLogging(fn) {
return (...args) => {
console.time(fn.name);
const result = fn(...args);
console.timeEnd(fn.name);
return result;
};
}
const sum = withLogging((a, b) => a + b);
sum(10, 20);The original function stays clean while additional behavior can be reused everywhere.
This technique is one of the foundations behind middleware, React higher-order components, and many modern frameworks.
Common mistake
Wrapping anonymous functions makes debugging harder because stack traces become less descriptive.
Whenever possible, decorate named functions instead.
function calculateTotal(price, tax) {
return price + tax;
}
const loggedCalculateTotal = withLogging(calculateTotal);Final Thoughts
Most JavaScript developers don’t need more libraries.
They need to make better use of the language they already have.
Optional chaining, nullish coalescing, logical assignment, lookup tables, modules, proxies, and decorators remove hundreds of lines of repetitive code across a typical project.
The biggest productivity improvement rarely comes from writing clever code.
It comes from letting modern JavaScript solve problems that developers used to solve manually.


