The 3 Refactoring Moves That Work on Any Legacy JavaScript Codebase
{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"After refactoring dozens of legacy JavaScript codebases — some over 10 years old — I've found that three moves solve about 80% of the pain. Here they are, in the order you should apply them."}]},{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"1. Write a characterization test before you touch anything"}]},{"type":"paragraph","content":[{"type":"text","text":"Don't try to understand the code first. Just call the function with known inputs and assert on whatever comes out. Even if the output looks wrong — that's the current behavior, and your job is to preserve it before changing structure."}]},{"type":"codeBlock","attrs":{"language":"javascript"},"content":[{"type":"text","text":"// Characterization test — lock in existing behavior\ntest('calculateTotal preserves legacy behavior', () => {\n const result = calculateTotal([{price: 10, qty: 2}, {price: 5, qty: 1}]);\n // Don't ask if 25.5 is \"correct\" — just lock it in\n expect(result).toBe(25.5);\n});"}]},{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"2. Extract Function — the single most useful refactoring"}]},{"type":"paragraph","content":[{"type":"text","text":"When you see a 200-line function, don't try to rewrite it. Find a block of 10-20 lines that does one thing, extract it into a named function, and run your tests. Repeat. This is mechanical and safe — the kind of refactoring that IDEs can automate, but understanding why it works makes you dangerous."}]},{"type":"codeBlock","attrs":{"language":"javascript"},"content":[{"type":"text","text":"// Before: buried inside a 200-line function\nlet discount = 0;\nif (user.tier === 'gold' && items.length > 3) {\n discount = subtotal 0.15;\n} else if (user.tier === 'silver') {\n discount = subtotal 0.05;\n}\n\n// After: extracted with a clear name\nconst discount = calculateTierDiscount(user.tier, items.length, subtotal);"}]},{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"3. Replace nested callbacks with async/await"}]},{"type":"paragraph","content":[{"type":"text","text":"Legacy Node.js code is full of callback pyramids. The conversion is mostly mechanical: wrap the callback-based function in a Promise, then await it. But the key insight is to do this one function at a time, from the innermost callback outward. Don't try to convert an entire file at once."}]},{"type":"codeBlock","attrs":{"language":"javascript"},"content":[{"type":"text","text":"// Before\ngetUser(id, (err, user) => {\n if (err) return cb(err);\n getOrders(user.id, (err, orders) => {\n if (err) return cb(err);\n cb(null, { user, orders });\n });\n});\n\n// After\nconst user = await getUser(id);\nconst orders = await getOrders(user.id);\nreturn { user, orders };"}]},{"type":"paragraph","content":[{"type":"text","text":"These three moves — characterization tests, extract function, and async/await conversion — will get you 80% of the way on any legacy JS codebase. The remaining 20% is where it gets interesting (dependency injection, strangler fig, module decomposition), and that's what we dig into in the full course."}]}]}
