Introduction
JavaScript moves fast. Every year brings new syntax, better APIs, and smarter patterns. But the fundamentals that separate good JavaScript from great JavaScript haven't changed: writing code that's readable, predictable, and easy to debug.
In this article, I'll share 10 practical tips I use daily. No fluff — just techniques you can apply to your next project today.
1. Use Optional Chaining and Nullish Coalescing Together
These two operators are a perfect pair for handling real-world API responses where fields may be missing:
// Old way — verbose and error-prone
const city = user && user.address && user.address.city
? user.address.city
: 'Unknown';
// Modern way — clean and safe
const city = user?.address?.city ?? 'Unknown';
// Works on methods too
const len = user?.getName?.()?.length ?? 0;
The ?. stops evaluation and returns undefined if anything in the chain is null or undefined. The ?? provides a fallback only for null/undefined (unlike || which also catches 0, '', and false).
2. Destructure Everything
Destructuring makes your intent explicit and reduces repetition:
// Function parameters
function renderUser({ name, email, role = 'viewer' }) {
return `${name} (${role}) — ${email}`;
}
// Rename while destructuring
const { name: userName, address: { city } = {} } = apiResponse;
// Swap variables without temp
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
// Skip array elements
const [first, , third] = [10, 20, 30];
3. Master Async/Await Error Handling
Most tutorials show the happy path. Here's a pattern for handling errors cleanly without try/catch everywhere:
// Helper that wraps any promise
async function to(promise) {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
}
// Usage — no nested try/catch needed
async function fetchUserData(userId) {
const [err, user] = await to(fetch(`/api/users/${userId}`).then(r => r.json()));
if (err) {
console.error('Failed to fetch user:', err.message);
return null;
}
const [profileErr, profile] = await to(fetchProfile(user.id));
if (profileErr) {
// Partial failure — still usable
return { ...user, profile: null };
}
return { ...user, profile };
}
This pattern comes from Go's error handling style and keeps your async code flat and readable.
4. Use Array Methods Instead of Loops
map, filter, reduce, and find are not just shortcuts — they communicate intent:
const orders = [
{ id: 1, status: 'paid', amount: 120 },
{ id: 2, status: 'pending', amount: 45 },
{ id: 3, status: 'paid', amount: 200 },
];
// Find total of paid orders
const paidTotal = orders
.filter(o => o.status === 'paid')
.reduce((sum, o) => sum + o.amount, 0);
// 320
// Group by status using reduce
const grouped = orders.reduce((acc, order) => {
(acc[order.status] ??= []).push(order);
return acc;
}, {});
// { paid: [...], pending: [...] }
5. Short-Circuit Evaluation for Conditional Rendering
In React and template strings, use && and || for inline conditions:
// Render only if condition is true
const element = isLoggedIn && <UserDashboard />;
// Provide default value (careful: 0 is falsy!)
const label = count || 'No items'; // BUG if count === 0
const label2 = count ?? 'No items'; // CORRECT
// Ternary for two outcomes
const badge = isPro ? <ProBadge /> : <FreeBadge />;
6. Object Spread for Immutable Updates
Never mutate objects directly in state or when you need to track changes:
const user = { name: 'Usman', role: 'admin', settings: { theme: 'dark' } };
// Shallow update
const updated = { ...user, role: 'viewer' };
// Deep update (nested objects need their own spread)
const withNewTheme = {
...user,
settings: { ...user.settings, theme: 'light' }
};
// Remove a key immutably
const { role, ...userWithoutRole } = user;
7. Use Promise.allSettled for Parallel Requests
Promise.all cancels everything if one request fails. Promise.allSettled waits for all and reports each outcome:
async function loadDashboard(userId) {
const [postsResult, statsResult, notificationsResult] = await Promise.allSettled([
fetchPosts(userId),
fetchStats(userId),
fetchNotifications(userId),
]);
return {
posts: postsResult.status === 'fulfilled' ? postsResult.value : [],
stats: statsResult.status === 'fulfilled' ? statsResult.value : null,
notifications: notificationsResult.status === 'fulfilled' ? notificationsResult.value : [],
};
}
Your dashboard still loads even if one API is down.
8. Debounce and Throttle Without Libraries
For search inputs and scroll handlers, you don't need Lodash:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
function throttle(fn, interval) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn(...args);
}
};
}
// Search that waits for the user to stop typing
const handleSearch = debounce(async (query) => {
const results = await searchAPI(query);
renderResults(results);
}, 300);
input.addEventListener('input', (e) => handleSearch(e.target.value));
9. Use WeakMap for Private Data
WeakMap keys must be objects and don't prevent garbage collection — perfect for storing metadata about DOM elements:
const elementData = new WeakMap();
function setTooltip(element, text) {
elementData.set(element, { tooltip: text, createdAt: Date.now() });
}
function getTooltip(element) {
return elementData.get(element)?.tooltip;
}
// When the element is removed from DOM, the WeakMap entry
// is automatically cleaned up — no memory leaks.
10. Type-Check Without TypeScript
When you can't use TypeScript, a few runtime checks go a long way:
function assertString(value, name) {
if (typeof value !== 'string') {
throw new TypeError(`${name} must be a string, got ${typeof value}`);
}
}
function assertPositiveNumber(value, name) {
if (typeof value !== 'number' || value <= 0 || isNaN(value)) {
throw new RangeError(`${name} must be a positive number`);
}
}
function createUser(name, age) {
assertString(name, 'name');
assertPositiveNumber(age, 'age');
return { name, age };
}
These assertions fail fast at the boundary, making bugs much easier to find.
Wrapping Up
JavaScript mastery is about knowing which tool to reach for and when. These 10 patterns cover the situations you'll face most often: handling missing data, managing async complexity, and writing code that's easy for your future self to read.
Start applying two or three of these this week. By next month, they'll be second nature.