Modern Syntax for Legacy Codebases
How AI modernizes old code automatically. Transform callbacks to async/await, var to const, CommonJS to ESM, and class components to hooks without breaking anything.
Modern Syntax for Legacy Codebases
Every codebase ages. The patterns that were best practice when the project started become outdated, then deprecated, then actively harmful. var becomes const. Callbacks become promises become async/await. CommonJS becomes ESM. Class components become functional components with hooks.
Manual modernization is tedious, error-prone, and never prioritized. There's always a feature to ship, a bug to fix, a deadline to meet. So the legacy syntax accumulates, making the codebase harder to read, harder to hire for, and harder to integrate with modern tools.
AI changes the economics. What used to take weeks of careful, manual refactoring now takes hours of AI-assisted transformation. The key is doing it systematically -- not file by file, but pattern by pattern.
Key Takeaways
- Pattern-based modernization is 10x faster than file-based -- identify a pattern, transform all instances, then move to the next pattern
- AI handles syntactic transformations with near-perfect accuracy -- var to const/let, callbacks to async/await, require to import
- Semantic transformations require human review -- changing error handling patterns, restructuring data flows, modifying API contracts
- The test suite is your safety net -- run tests after each pattern transformation, not after all transformations
- Incremental modernization beats big-bang rewrites -- modernize one pattern per sprint alongside normal feature work
The Pattern-Based Approach
Why Files Don't Matter
The natural instinct is to modernize file by file: "Let's start with src/api/users.js and modernize everything in it." This approach fails because:
- A single file uses multiple patterns (callbacks AND var AND CommonJS)
- Modernizing one pattern might conflict with another in the same file
- Cross-file dependencies mean a pattern change in one file requires changes in others
- It's impossible to estimate progress when each file has unknown scope
Instead, modernize pattern by pattern across the entire codebase:
- Pass 1: Convert all
vartoconst/let - Pass 2: Convert all
require()toimport - Pass 3: Convert all callbacks to async/await
- Pass 4: Convert all class components to functional components
Each pass is a single, focused transformation that can be validated independently.
Setting Up the Pipeline
Before starting any modernization, ensure your safety net is in place:
# Verify tests pass before starting
npm test
# Create a modernization branch
git checkout -b modernize/var-to-const
# Set up a pre-commit hook that runs affected tests
echo 'npm test -- --related' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
Pass 1: var to const/let
This is the safest transformation and the best starting point because it has no behavioral impact when done correctly.
The Rules
varwith no reassignment becomesconstvarwith reassignment becomesletvarin for loops becomeslet(scoping changes from function to block)
What AI Gets Right
AI handles the basic transformation perfectly:
// Before
var users = getUsers()
var count = users.length
var i
for (i = 0; i < count; i++) {
var user = users[i]
console.log(user.name)
}
// After
const users = getUsers()
const count = users.length
for (let i = 0; i < count; i++) {
const user = users[i]
console.log(user.name)
}
What AI Gets Wrong
The scoping change from var (function-scoped) to let/const (block-scoped) can break code that relies on hoisting:
// This works with var (hoisting)
console.log(config) // undefined but doesn't throw
var config = loadConfig()
// This breaks with const/let (temporal dead zone)
console.log(config) // ReferenceError!
const config = loadConfig()
AI should detect this pattern and flag it rather than blindly transforming. Include this constraint in your modernization skill:
## Constraint
When converting var to const/let:
- Flag any var usage that relies on hoisting (used before declaration)
- Do not convert vars inside `catch` blocks without checking scope usage
- Preserve var for function-scoped variables that are used across block boundaries
Pass 2: CommonJS to ESM
Converting require() to import is the most impactful modernization because it unlocks tree-shaking, static analysis, and compatibility with modern tooling.
The Transformation
// Before (CommonJS)
const express = require('express')
const { Router } = require('express')
const path = require('path')
const myModule = require('./myModule')
module.exports = { handler }
module.exports.helper = helperFn
// After (ESM)
import express, { Router } from 'express'
import path from 'path'
import myModule from './myModule'
export { handler }
export { helperFn as helper }
Gotchas AI Must Handle
Dynamic requires: require() can be called conditionally or with computed paths. import() can handle the dynamic case but has different semantics:
// CommonJS dynamic require
const plugin = require(`./plugins/${pluginName}`)
// ESM dynamic import (returns a promise)
const plugin = await import(`./plugins/${pluginName}`)
Circular dependencies: CommonJS handles circular dependencies by returning a partially-initialized module. ESM throws a reference error. AI should detect circular imports and flag them for manual review.
__dirname and __filename: These don't exist in ESM. The replacement:
// ESM equivalent
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
AI that handles these gotchas automatically produces transformations that work on the first try. This is where skill guardrails make the difference between a helpful transformation and a broken one.
Pass 3: Callbacks to Async/Await
This is the highest-value transformation for code readability, but also the most complex.
Simple Callbacks
// Before
function getUser(id, callback) {
db.query('SELECT * FROM users WHERE id = ?', [id], (err, results) => {
if (err) return callback(err)
callback(null, results[0])
})
}
// After
async function getUser(id) {
const results = await db.query('SELECT * FROM users WHERE id = ?', [id])
return results[0]
}
Nested Callbacks (Callback Hell)
// Before
getUser(id, (err, user) => {
if (err) return handleError(err)
getOrders(user.id, (err, orders) => {
if (err) return handleError(err)
getPayments(orders[0].id, (err, payments) => {
if (err) return handleError(err)
renderInvoice(user, orders, payments)
})
})
})
// After
try {
const user = await getUser(id)
const orders = await getOrders(user.id)
const payments = await getPayments(orders[0].id)
renderInvoice(user, orders, payments)
} catch (err) {
handleError(err)
}
What AI Must Not Touch
Event emitters: Not all callbacks are meant to be awaited. Event handlers (emitter.on('data', callback)) should remain as callbacks.
Stream processors: stream.on('data', chunk => { ... }) should not be converted to async/await without restructuring the entire stream processing logic.
setTimeout/setInterval: These are scheduling callbacks, not asynchronous operations. Converting them requires a different pattern (wrapping in a Promise).
Include these exceptions in the modernization skill's constraints.
Pass 4: Class Components to Hooks
For React codebases, converting class components to functional components with hooks is a common modernization:
// Before
class UserProfile extends React.Component {
state = { loading: true, user: null }
componentDidMount() {
fetchUser(this.props.id).then(user => {
this.setState({ loading: false, user })
})
}
render() {
if (this.state.loading) return <Spinner />
return <div>{this.state.user.name}</div>
}
}
// After
function UserProfile({ id }) {
const [loading, setLoading] = useState(true)
const [user, setUser] = useState(null)
useEffect(() => {
fetchUser(id).then(user => {
setLoading(false)
setUser(user)
})
}, [id])
if (loading) return <Spinner />
return <div>{user.name}</div>
}
AI handles this transformation well for simple components. Complex components with lifecycle methods like shouldComponentUpdate, getSnapshotBeforeUpdate, or componentDidCatch require more careful handling with React.memo, refs, and error boundaries.
Measuring Progress
Track modernization progress with simple metrics:
# Count remaining legacy patterns
echo "var declarations: $(grep -r 'var ' src/ --include='*.js' -l | wc -l) files"
echo "require() calls: $(grep -r 'require(' src/ --include='*.js' -l | wc -l) files"
echo "Callback patterns: $(grep -r 'callback(' src/ --include='*.js' -l | wc -l) files"
echo "Class components: $(grep -r 'extends React.Component' src/ --include='*.tsx' -l | wc -l) files"
Add these metrics to your CI pipeline so the team can see modernization progress alongside feature development.
FAQ
Should I modernize the entire codebase at once?
No. Modernize pattern by pattern, and consider doing one pattern per sprint alongside normal feature work. Big-bang modernization creates massive PRs that are impossible to review and risky to merge.
What if the test suite doesn't cover legacy code?
Write tests before modernizing. The test writer skill can generate tests for legacy code, capturing its current behavior. Then modernize with confidence that you're preserving behavior.
How do I handle files that mix old and new patterns?
This is normal during incremental modernization. A file might have ESM imports at the top and callback-style code in the body. Each modernization pass addresses its specific pattern regardless of what else is in the file.
Will modernization break production?
Not if you run tests after each pattern pass and deploy incrementally. Syntactic transformations (var to const) are extremely safe. Semantic transformations (callbacks to async/await) require more careful testing.
How long does a full modernization take?
For a 50k-line codebase: var to const takes 1-2 hours, CommonJS to ESM takes 4-8 hours, callbacks to async/await takes 1-2 days. AI handles the mechanical transformation; humans review the edge cases.
Sources
- Node.js ESM Documentation -- Official ESM migration guide
- React Hooks Migration -- Class to hooks conversion patterns
- JavaScript.info: Modern JavaScript -- Modern syntax reference
- TypeScript Handbook -- TypeScript modernization patterns
Explore production-ready AI skills at aiskill.market/browse or submit your own skill to the marketplace.