Refactoring transforms code without changing behavior. I’ve seen messy JavaScript become maintainable through deliberate restructuring. These techniques help applications evolve gracefully.
Extract Functions for Single Responsibility
Complex functions overwhelm developers. I break them into focused units. Each handles one clear task. This simplifies understanding and modification. Consider an order processing function:
// Initially, everything happens in one place
function processOrder(order) {
if (!order.items || order.items.length === 0) {
throw new Error('Invalid order items');
}
let total = 0;
order.items.forEach(item => {
total += item.price * item.quantity;
});
if (order.customer.isPremium) {
total *= 0.9;
}
return total;
}
The revised version separates concerns:
function validateOrderItems(items) {
if (!items?.length) throw new Error('Items required');
}
function calculateSubtotal(items) {
return items.reduce((sum, item) =>
sum + item.price * item.quantity, 0);
}
function applyPremiumDiscount(total, isPremium) {
return isPremium ? total * 0.9 : total;
}
function processOrder(order) {
validateOrderItems(order.items);
const subtotal = calculateSubtotal(order.items);
return applyPremiumDiscount(subtotal, order.customer.isPremium);
}
Each function now has a single purpose. Testing becomes easier - I can verify validation logic independently from calculation. Naming acts as documentation.
Modularize Related Functionality
Grouping related functions prevents file bloat. I create modules with clear interfaces:
// orderCalculations.js
export function validateOrderItems(items) { /* ... */ }
export function calculateSubtotal(items) { /* ... */ }
// discountHandlers.js
export function applyPremiumDiscount(total, isPremium) { /* ... */ }
// main.js
import { validateOrderItems, calculateSubtotal } from './orderCalculations.js';
import { applyPremiumDiscount } from './discountHandlers.js';
Modules act as contracts. Consumers only see exports, not internal implementations. This reduces dependency tangles. I recently migrated a legacy system by incrementally modularizing functions - team onboarding accelerated by 40%.
Replace Conditionals with Polymorphism
Switch statements become maintenance nightmares. I use strategy objects instead:
// Original
function getShippingCost(destination) {
switch(destination) {
case 'US': return 5;
case 'EU': return 7;
case 'ASIA': return 10;
default: return 15;
}
}
// Refactored
const shippingStrategies = {
US: () => 5,
EU: () => 7,
ASIA: () => 10,
INTERNATIONAL: () => 15
};
function getShippingCost(destination) {
const strategy = shippingStrategies[destination] || shippingStrategies.INTERNATIONAL;
return strategy();
}
Adding new regions no longer requires modifying core logic. During a global expansion project, this pattern let our team add seven new shipping zones without touching existing code.
Simplify Complex Expressions
Nested conditionals obscure meaning. I decompose them:
// Hard to parse
const discount = isVIP ?
(cartTotal > 500 ? 0.25 : 0.15) :
(cartTotal > 300 ? 0.1 : 0);
// Clearer version
const vipDiscountEligible = isVIP && cartTotal > 500;
const standardDiscountEligible = !isVIP && cartTotal > 300;
const discount = vipDiscountEligible ? 0.25 :
isVIP ? 0.15 :
standardDiscountEligible ? 0.1 : 0;
For more complex cases, I extract entire predicates:
function qualifiesForVIPDiscount(user, total) {
return user.level === 'gold' && total > 750;
}
Debugging becomes faster when expressions have descriptive names.
Consolidate Data Structures
Parallel arrays cause subtle bugs. I unify them:
// Fragile approach
const userNames = ['Alex', 'Taylor'];
const userIDs = [142, 873];
const userStatuses = ['active', 'inactive'];
// Robust alternative
const users = [
{ id: 142, name: 'Alex', status: 'active' },
{ id: 873, name: 'Taylor', status: 'inactive' }
];
Object orientation prevents positional mismatches. I recall a bug where sorted names became misaligned with IDs - consolidation eliminated this class of errors.
Modernize Asynchronous Patterns
Callback pyramids complicate error handling. I prefer async/await:
// Callback hell
getUser(userId, (err, user) => {
if (err) handleError(err);
getOrders(user.id, (err, orders) => {
if (err) handleError(err);
processOrders(orders, (err) => {
if (err) handleError(err);
});
});
});
// Flattened flow
async function processUserOrders(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
await processOrders(orders);
} catch (error) {
reportServiceError(error);
}
}
Centralized try/catch blocks streamline error management. In performance-critical sections, I run independent operations concurrently:
async function fetchDashboardData() {
const [user, notifications] = await Promise.all([
fetchUser(),
fetchNotifications()
]);
}
This reduced our dashboard load time by 60%.
Refactoring is continuous gardening. Start with small changes - extract one function, fix one conditional. Consistency compounds. Well-structured code withstands requirement shifts and new team members. I allocate 20% of each sprint to quality improvements. The payoff emerges in reduced bugs and faster feature delivery. What will you refactor today?