Ask AI to build a feature and you will often get back one massive function that handles input validation, data transformation, APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. calls, error handling, and UI updates all in one block. It works, but it is the code equivalent of a run-on sentence. You can read it if you concentrate, but breaking it into paragraphs makes everything clearer.
This lesson teaches you the most impactful refactoringWhat is refactoring?Restructuring existing code to make it cleaner, more readable, or more efficient without changing what it does. skill: taking large, tangled code and breaking it into small, focused pieces. It is not about making code "elegant." It is about making code you can change safely, test easily, and understand quickly.
Why AI generates monolithic functions
AI models generate code sequentially, one tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. at a time, top to bottom. They do not plan a function architecture and then fill in the pieces. They start writing and keep going until the feature is complete. The result is code that reads like a streamWhat is stream?A way to process data in small chunks as it arrives instead of loading everything into memory at once, keeping memory usage low for large files. of consciousness.
// AI-generated: one function does everything
async function handleCheckout(cart, user) {
// Validate cart
if (!cart.items || cart.items.length === 0) {
throw new Error('Cart is empty');
}
for (const item of cart.items) {
if (item.quantity <= 0) {
throw new Error(`Invalid quantity for ${item.name}`);
}
if (item.price < 0) {
throw new Error(`Invalid price for ${item.name}`);
}
}
// Calculate totals
let subtotal = 0;
for (const item of cart.items) {
subtotal += item.price * item.quantity;
}
const tax = subtotal * 0.08875;
const shipping = subtotal > 50 ? 0 : 5.99;
const total = subtotal + tax + shipping;
// Check inventory
for (const item of cart.items) {
const stock = await fetch(`/api/inventory/${item.id}`);
const data = await stock.json();
if (data.available < item.quantity) {
throw new Error(`${item.name} is out of stock`);
}
}
// Process payment
const payment = await fetch('/api/payments', {
method: 'POST',
body: JSON.stringify({ amount: total, userId: user.id })
});
if (!payment.ok) {
throw new Error('Payment failed');
}
// Create order
const order = await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({
userId: user.id,
items: cart.items,
subtotal,
tax,
shipping,
total
})
});
return order.json();
}This function is 45 lines and handles five distinct responsibilities. If the tax calculation changes, you have to read the whole function to find it. If the payment APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. changes, you are editing the same file as inventory logic. If you want to test the total calculation, you cannot, it is wired to real API calls.
The single-responsibility principle in plain English
The single-responsibility principle says: a function should have one reason to change. Not one line of code, not one operation, one reason to change. If two different business requirements could independently cause you to edit the same function, it is doing too much.
In the checkout example above, the function changes if: the validation rules change, the tax rate changes, the shipping threshold changes, the inventory APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. changes, the payment API changes, or the order creation format changes. That is six reasons to change, so it should be at least six functions.
Two tests for "too long"
You do not need a strict line count rule. Two quick tests tell you if a function needs splitting.
The scroll test
If you cannot see the entire function on your screen without scrolling, it is probably too long. This is not a hard rule, but it is a reliable signal. Functions you can see all at once are functions you can reason about all at once.
The "and" test
Describe what the function does in one sentence. If you use the word "and," the function has multiple responsibilities.
| Description | Verdict |
|---|---|
| "Validates the cart" | Single responsibility, fine |
| "Validates the cart and calculates the total" | Two responsibilities, split it |
| "Validates the cart and calculates the total and processes payment and creates the order" | Four responsibilities, definitely split it |
How to extract: step by step
Extraction is the core refactoringWhat is refactoring?Restructuring existing code to make it cleaner, more readable, or more efficient without changing what it does. move. You identify a chunk of code that does one thing, give it a name, and pull it into its own function.
Step 1: identify the chunks
Look for blank lines and comments in the original function. AI often puts a comment before each logical section, those comments are your extraction boundaries.
async function handleCheckout(cart, user) {
// Validate cart <- chunk 1
...
// Calculate totals <- chunk 2
...
// Check inventory <- chunk 3
...
// Process payment <- chunk 4
...
// Create order <- chunk 5
...
}Step 2: name them
Each chunk becomes a function. The comment that described the chunk becomes the function name.
function validateCart(cart) { ... }
function calculateOrderTotals(items) { ... }
async function checkInventory(items) { ... }
async function processPayment(total, userId) { ... }
async function createOrder(orderData) { ... }Step 3: pull them out
Move each chunk into its own function, passing in only the data it needs.
function validateCart(cart) {
if (!cart.items || cart.items.length === 0) {
throw new Error('Cart is empty');
}
for (const item of cart.items) {
if (item.quantity <= 0) {
throw new Error(`Invalid quantity for ${item.name}`);
}
if (item.price < 0) {
throw new Error(`Invalid price for ${item.name}`);
}
}
}
function calculateOrderTotals(items) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * 0.08875;
const shipping = subtotal > 50 ? 0 : 5.99;
const total = subtotal + tax + shipping;
return { subtotal, tax, shipping, total };
}The orchestrating function becomes a readable summary:
async function handleCheckout(cart, user) {
validateCart(cart);
const totals = calculateOrderTotals(cart.items);
await checkInventory(cart.items);
await processPayment(totals.total, user.id);
return createOrder({ userId: user.id, items: cart.items, ...totals });
}Now handleCheckout reads like a checklist. You can understand the entire flow in five seconds. Each step is testable independently. When the tax rate changes, you edit calculateOrderTotals without touching payment or inventory logic.
Simplifying conditionals
AI loves nested conditionals. Three levels of if inside a for loop inside a try block. The fix is usually one of three techniques.
Early returns (guard clauses)
Instead of nesting, check for invalid conditions first and return early.
// AI-generated: deeply nested
function getDiscount(user, order) {
if (user) {
if (user.isPremium) {
if (order.total > 100) {
return 0.2;
} else {
return 0.1;
}
} else {
if (order.total > 200) {
return 0.05;
} else {
return 0;
}
}
} else {
return 0;
}
}
// Refactored: flat with early returns
function getDiscount(user, order) {
if (!user) return 0;
if (user.isPremium) {
return order.total > 100 ? 0.2 : 0.1;
}
return order.total > 200 ? 0.05 : 0;
}The refactored version handles the same cases with half the lines and no nesting beyond one level.
Lookup objects instead of switch/if chains
When you have many branches that map a value to a result, use an object.
// AI-generated: long switch
function getStatusColor(status) {
switch (status) {
case 'active': return 'green';
case 'pending': return 'yellow';
case 'cancelled': return 'red';
case 'completed': return 'blue';
case 'draft': return 'gray';
default: return 'gray';
}
}
// Refactored: lookup object
const STATUS_COLORS = {
active: 'green',
pending: 'yellow',
cancelled: 'red',
completed: 'blue',
draft: 'gray',
};
function getStatusColor(status) {
return STATUS_COLORS[status] ?? 'gray';
}The lookup object is easier to scan, easier to extend (just add a line), and separates the data from the logic.
Extract complex boolean expressions
When a condition has multiple parts joined by && and ||, give it a name.
// AI-generated: hard to read condition
if (user.role === 'admin' || (user.role === 'editor' && user.department === 'content' && !user.suspended)) {
allowEdit();
}
// Refactored: named conditions
const isAdmin = user.role === 'admin';
const isActiveContentEditor =
user.role === 'editor' &&
user.department === 'content' &&
!user.suspended;
if (isAdmin || isActiveContentEditor) {
allowEdit();
}|| becomes an &&, a > becomes >=, or an edge case gets dropped. Always verify that simplified conditionals produce the same results for all inputs, especially boundary values.Removing dead codeWhat is dead code?Code that exists in the project but is never executed or referenced, adding confusion without serving a purpose.
AI frequently generates code that is never used. Unused imports, variables assigned but never read, functions defined but never called, commented-out blocks from a previous approach. This is noise that makes the real code harder to find.
// AI left all of this behind
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [relatedProducts, setRelatedProducts] = useState([]); // never used
const prevProductId = useRef(productId); // never used
// const [showReviews, setShowReviews] = useState(false);
// const toggleReviews = () => setShowReviews(!showReviews);
useEffect(() => {
fetchProduct(productId);
}, [productId]);
// ...
}Delete unused imports, unused state variables, and commented-out code. If you need the old code, it is in git history. Commented-out code in a live file is just visual clutter that slows down everyone who reads it.
| Dead code type | How to spot it | Action |
|---|---|---|
| Unused imports | Grayed out in VS Code, or noUnusedLocals flag | Delete |
| Unused variables | TypeScript compiler warning | Delete |
| Commented-out code | Visible // blocks with actual code | Delete (git has history) |
| Unreachable code | Code after an unconditional return | Delete |
| Unused functions | No callers in the project (search for usage) | Delete or move to a separate file if you think you will need it later |
The extraction checklist
After refactoringWhat is refactoring?Restructuring existing code to make it cleaner, more readable, or more efficient without changing what it does., verify your work with these questions:
Can you describe each function in one sentence without using "and"? Are all functions shorter than your screen? Can you understand the orchestrating function without reading the helpers? Did you introduce any new bugs? (Run your tests or manually verify.) Is there any dead codeWhat is dead code?Code that exists in the project but is never executed or referenced, adding confusion without serving a purpose. left?
If you answer yes to all of these, the extraction is complete. The code is not just shorter, it is code you can change with confidence.