Skip to main content

Modern JavaScript: Writing Code That Doesn'\''t Hurt

February 18, 2026

{}=>

JavaScript powers the web, but writing good JavaScript requires discipline. Whether you're building frontends, backends, or full-stack apps, these best practices will help you write cleaner, safer, and more maintainable code.

Variables and Declarations

Use const by Default, let When Needed

// Bad
var name = 'John';
var items = [1, 2, 3];

// Good
const name = 'John';       // Never reassigned
const items = [1, 2, 3];   // Array reference doesn't change
let count = 0;              // Will be reassigned
count++;

Use Descriptive Names

// Bad
const d = new Date();
const u = users.filter((x) => x.a > 18);

// Good
const currentDate = new Date();
const adultUsers = users.filter((user) => user.age > 18);

Avoid Magic Numbers

// Bad
if (password.length < 8) { ... }
setTimeout(retry, 3000);

// Good
const MIN_PASSWORD_LENGTH = 8;
const RETRY_DELAY_MS = 3000;

if (password.length < MIN_PASSWORD_LENGTH) { ... }
setTimeout(retry, RETRY_DELAY_MS);

Functions

Keep Functions Small and Focused

// Bad - does too many things
function processUser(userData) {
  // validate
  if (!userData.email) throw new Error('Email required');
  if (!userData.name) throw new Error('Name required');
  // normalize
  userData.email = userData.email.toLowerCase().trim();
  userData.name = userData.name.trim();
  // save
  return db.users.insert(userData);
}

// Good - separated concerns
function validateUser(data) {
  if (!data.email) throw new Error('Email required');
  if (!data.name) throw new Error('Name required');
}

function normalizeUser(data) {
  return {
    ...data,
    email: data.email.toLowerCase().trim(),
    name: data.name.trim(),
  };
}

function createUser(userData) {
  validateUser(userData);
  const normalized = normalizeUser(userData);
  return db.users.insert(normalized);
}

Use Default Parameters

// Bad
function createUser(name, role) {
  role = role || 'user';
}

// Good
function createUser(name, role = 'user') {
  // ...
}

Use Arrow Functions for Callbacks

// Good - arrow functions for short callbacks
const names = users.map((user) => user.name);
const adults = users.filter((user) => user.age >= 18);
const total = prices.reduce((sum, price) => sum + price, 0);

Return Early to Avoid Nesting

// Bad - deeply nested
function getDiscount(user) {
  if (user) {
    if (user.isPremium) {
      if (user.yearsActive > 5) {
        return 0.3;
      } else {
        return 0.2;
      }
    } else {
      return 0.05;
    }
  } else {
    return 0;
  }
}

// Good - early returns
function getDiscount(user) {
  if (!user) return 0;
  if (!user.isPremium) return 0.05;
  if (user.yearsActive > 5) return 0.3;
  return 0.2;
}

Objects and Arrays

Use Destructuring

// Bad
const name = user.name;
const email = user.email;
const first = items[0];

// Good
const { name, email } = user;
const [first] = items;

// Function parameters
function greet({ name, age }) {
  return `Hello ${name}, you are ${age}`;
}

Use Spread for Immutable Updates

// Objects
const updated = { ...user, name: 'Jane' };

// Arrays
const added = [...items, newItem];
const removed = items.filter((item) => item.id !== targetId);
const replaced = items.map((item) =>
  item.id === targetId ? { ...item, done: true } : item,
);

Use Optional Chaining and Nullish Coalescing

// Bad
const city = user && user.address && user.address.city;
const name = user.name !== null && user.name !== undefined ? user.name : 'Anonymous';

// Good
const city = user?.address?.city;
const name = user.name ?? 'Anonymous';

Use Map and Set When Appropriate

// Use Map for key-value pairs with non-string keys or frequent additions/deletions
const cache = new Map();
cache.set(userId, userData);
cache.has(userId); // true

// Use Set for unique values
const uniqueTags = new Set(['js', 'react', 'js']); // Set(2) {'js', 'react'}
const hasTag = uniqueTags.has('react'); // true

Async Patterns

Use async/await Over Raw Promises

// Bad - promise chains
function getUser(id) {
  return fetch(`/api/users/${id}`)
    .then((res) => res.json())
    .then((user) => fetch(`/api/posts?userId=${user.id}`))
    .then((res) => res.json());
}

// Good - async/await
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  const user = await res.json();
  const postsRes = await fetch(`/api/posts?userId=${user.id}`);
  return postsRes.json();
}

Run Independent Promises in Parallel

// Bad - sequential when they could be parallel
const users = await getUsers();
const posts = await getPosts();

// Good - parallel
const [users, posts] = await Promise.all([getUsers(), getPosts()]);

Use Promise.allSettled When Failures Are Acceptable

const results = await Promise.allSettled([
  fetchFromPrimary(),
  fetchFromBackup(),
  fetchFromCache(),
]);

const successes = results
  .filter((r) => r.status === 'fulfilled')
  .map((r) => r.value);

Always Handle Async Errors

// Bad - unhandled rejection
async function loadData() {
  const res = await fetch('/api/data');
  return res.json();
}

// Good - proper error handling
async function loadData() {
  try {
    const res = await fetch('/api/data');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (error) {
    console.error('Failed to load data:', error);
    return null;
  }
}

Error Handling

Use Custom Error Classes

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, 404);
    this.name = 'NotFoundError';
  }
}

class ValidationError extends AppError {
  constructor(message, fields) {
    super(message, 400);
    this.name = 'ValidationError';
    this.fields = fields;
  }
}

Don't Swallow Errors

// Bad - silently swallows the error
try {
  await saveData();
} catch (e) {
  // do nothing
}

// Good - handle or rethrow
try {
  await saveData();
} catch (error) {
  if (error instanceof ValidationError) {
    showFieldErrors(error.fields);
  } else {
    throw error; // Rethrow unexpected errors
  }
}

Modules

Use ES Modules

// Named exports for utilities
export function formatDate(date) { ... }
export function parseDate(str) { ... }

// Default export for main component/class
export default class UserService { ... }

// Import what you need
import UserService from './user-service.js';
import { formatDate } from './utils.js';

Avoid Circular Dependencies

// Bad - A imports B, B imports A
// a.js
import { helperB } from './b.js';
// b.js
import { helperA } from './a.js';

// Good - extract shared logic
// shared.js
export function sharedHelper() { ... }
// a.js
import { sharedHelper } from './shared.js';
// b.js
import { sharedHelper } from './shared.js';

Performance

Debounce Expensive Operations

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const handleSearch = debounce((query) => {
  fetch(`/api/search?q=${encodeURIComponent(query)}`);
}, 300);

input.addEventListener('input', (e) => handleSearch(e.target.value));

Use for...of Over forEach for Performance

// forEach - creates a callback for each iteration
items.forEach((item) => process(item));

// for...of - no callback overhead, supports break/continue
for (const item of items) {
  if (item.skip) continue;
  process(item);
  if (item.last) break;
}

Avoid Memory Leaks

// Always clean up event listeners
const controller = new AbortController();

element.addEventListener('click', handler, { signal: controller.signal });

// Later, clean up all listeners at once
controller.abort();

// Always clean up timers
const intervalId = setInterval(poll, 5000);
// Later
clearInterval(intervalId);

Security

Sanitize User Input

// Never insert user input as HTML
// Bad
element.innerHTML = userInput;

// Good
element.textContent = userInput;

// If HTML is needed, sanitize first
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

Use encodeURIComponent for URLs

// Bad - vulnerable to injection
const url = `/search?q=${query}`;

// Good
const url = `/search?q=${encodeURIComponent(query)}`;

Avoid eval and Function Constructor

// Never do this
eval(userInput);
new Function(userInput)();

// Use safer alternatives like JSON.parse for data
const data = JSON.parse(jsonString);

Modern JavaScript Features

Use Logical Assignment Operators

// Nullish coalescing assignment
user.name ??= 'Anonymous';

// OR assignment
config.debug ||= false;

// AND assignment
user.verified &&= checkVerification();

Use structuredClone for Deep Copies

// Bad - doesn't handle nested objects
const copy = { ...original };

// Bad - slow and loses special types
const copy = JSON.parse(JSON.stringify(original));

// Good - native deep clone
const copy = structuredClone(original);

Use Array.at() for Negative Indexing

const items = [1, 2, 3, 4, 5];

// Old way
const last = items[items.length - 1]; // 5

// Modern way
const last = items.at(-1);  // 5
const secondLast = items.at(-2); // 4

Quick Reference

PracticeWhy
const by defaultPrevents accidental reassignment
Descriptive namesSelf-documenting code
Early returnsReduces nesting and complexity
DestructuringCleaner variable extraction
async/awaitReadable async code
Promise.all for parallel workFaster execution
Custom error classesStructured error handling
ES modulesTree-shaking, static analysis
structuredCloneSafe deep copies
Sanitize user inputPrevent XSS attacks

Summary

Writing good JavaScript comes down to:

  1. Be explicit — descriptive names, const/let, early returns
  2. Be immutable — spread operators, map/filter over mutation
  3. Be async-awareasync/await, parallel promises, proper error handling
  4. Be secure — sanitize input, encode URLs, avoid eval
  5. Be modern — use the latest language features for cleaner code

Building with a framework? Read Building Production-Ready Apps with Next.js and Understanding React Server Components to see how these fundamentals apply in practice.

Recommended Posts