Cursor can automatically convert callback-based Node.js code into modern async/await patterns when given the right prompts and context. This tutorial shows how to use Composer (Cmd+I) to refactor legacy callback code file by file, preserving error handling and ensuring backward compatibility throughout the migration.
Modernizing callback-based Node.js code with Cursor
Many Node.js projects still contain callback-heavy code from the pre-Promise era. Manually converting nested callbacks to async/await is tedious and error-prone. Cursor's Composer Agent mode can refactor entire files while preserving error semantics. This tutorial walks through setting up rules, prompting effectively, and verifying the output.
Prerequisites
- Cursor installed with a Node.js project open
- Project contains callback-based code to refactor
- Node.js 16+ for native async/await support
- Git initialized for safe rollback
Step-by-step guide
Add async refactoring rules to your project
Add async refactoring rules to your project
Create a rule file that tells Cursor how to handle async conversions consistently. This ensures every refactored file follows the same pattern for error handling, return types, and naming conventions. Place this in .cursor/rules/ so it auto-applies to relevant files.
1---2description: Async/await conversion rules for Node.js3globs: "*.js,*.ts"4alwaysApply: false5---67## Async Refactoring Rules8- Convert callback patterns to async/await, NEVER to raw Promises with .then()9- Wrap awaited calls in try/catch blocks10- Preserve original error messages and error codes11- Use util.promisify() for Node.js core APIs (fs, child_process)12- Keep function signatures backward-compatible where possible13- Add JSDoc @returns {Promise<T>} annotations to converted functions14- Do NOT change the function name during conversionExpected result: A rule file that Cursor will auto-attach when you edit .js or .ts files.
Commit your current code before refactoring
Commit your current code before refactoring
Before letting Cursor modify any files, commit everything to Git. This is critical because Composer Agent mode can modify multiple files in a single operation. If the refactoring introduces issues, you can revert the entire batch instantly. Open the terminal in Cursor and run the commit command.
1git add -A && git commit -m "checkpoint: before async/await migration"Pro tip: Make a git commit before every Cursor Agent operation. The Agent can modify files you did not ask it to touch.
Expected result: A clean Git checkpoint you can revert to if the refactoring goes wrong.
Refactor a single file using Cmd+K
Refactor a single file using Cmd+K
Start with one file to verify the pattern. Open a callback-heavy file, select the function you want to convert, press Cmd+K, and type the conversion prompt. Review the diff before accepting. This lets you establish the pattern before scaling to multiple files.
1// Original callback code:2function getUser(id, callback) {3 db.query('SELECT * FROM users WHERE id = ?', [id], (err, rows) => {4 if (err) return callback(err);5 if (!rows.length) return callback(new Error('User not found'));6 callback(null, rows[0]);7 });8}910// Cursor Cmd+K prompt:11// "Convert this callback function to async/await. Use try/catch12// for error handling. Keep the same function name."1314// Expected Cursor output:15async function getUser(id) {16 try {17 const rows = await db.query('SELECT * FROM users WHERE id = ?', [id]);18 if (!rows.length) {19 throw new Error('User not found');20 }21 return rows[0];22 } catch (err) {23 throw err;24 }25}Expected result: Cursor shows a diff replacing the callback pattern with async/await. Review and accept with Enter.
Scale to multiple files with Composer Agent
Scale to multiple files with Composer Agent
Once you are happy with the single-file result, use Composer Agent mode (Cmd+I) to refactor multiple files. Reference the folder containing your callback-based code and give Cursor a clear, scoped instruction. Agent mode will iterate through files, applying consistent transformations.
1// Composer Agent prompt (Cmd+I):2// @src/services Refactor all callback-based functions in this3// folder to async/await. Follow these rules:4// 1. Convert one file at a time5// 2. Use try/catch for all error handling6// 3. Use util.promisify for fs and child_process calls7// 4. Update the callers in the same file8// 5. Do NOT modify test files9// 6. Wait for my approval between each filePro tip: Add 'Wait for my approval between each file' to prevent the Agent from making too many changes at once. Review each file before it moves on.
Expected result: Cursor Agent converts each service file one at a time, showing diffs for your approval.
Update callers throughout the codebase
Update callers throughout the codebase
After converting the service functions, their callers also need updating. Use Cursor Chat (Cmd+L) with @codebase to find all callers and generate the necessary changes. This catches any file that was passing callbacks to the now-async functions.
1// Cursor Chat prompt (Cmd+L):2// @codebase Find all files that call getUser(), getOrder(),3// or getProduct() with a callback argument. Show me each4// call site and generate the updated code using await instead5// of callbacks. Include the necessary async keyword on the6// parent function.Expected result: Cursor lists every caller and provides updated code with await syntax.
Run tests to verify the refactoring
Run tests to verify the refactoring
Execute your test suite to confirm the refactored code behaves identically to the original. If tests fail, paste the error output into Cursor Chat (Cmd+L) and ask it to fix the issue. The key validation is that all async functions return Promises and error handling is preserved.
1// Terminal command:2npm test34// If tests fail, paste the error into Cursor Chat:5// @src/services/userService.js The test for getUser is failing6// with 'TypeError: callback is not a function'. The function7// was converted from callback to async/await but some callers8// still pass a callback. Fix the callers.Expected result: All tests pass, confirming the async/await migration preserved original behavior.
Complete working example
1const { promisify } = require('util');2const fs = require('fs');3const readFile = promisify(fs.readFile);45// Refactored from callback-based to async/await67/**8 * @param {string} id9 * @returns {Promise<object>} User object10 */11async function getUser(id) {12 try {13 const rows = await db.query(14 'SELECT * FROM users WHERE id = ?',15 [id]16 );17 if (!rows.length) {18 throw new Error(`User not found: ${id}`);19 }20 return rows[0];21 } catch (err) {22 console.error(`Failed to fetch user ${id}:`, err.message);23 throw err;24 }25}2627/**28 * @param {string} userId29 * @param {object} updates30 * @returns {Promise<object>} Updated user object31 */32async function updateUser(userId, updates) {33 try {34 const user = await getUser(userId);35 const merged = { ...user, ...updates, updatedAt: new Date() };36 await db.query(37 'UPDATE users SET ? WHERE id = ?',38 [merged, userId]39 );40 return merged;41 } catch (err) {42 console.error(`Failed to update user ${userId}:`, err.message);43 throw err;44 }45}4647/**48 * @param {string} path49 * @returns {Promise<object>} Parsed config50 */51async function loadConfig(path) {52 const data = await readFile(path, 'utf-8');53 return JSON.parse(data);54}5556module.exports = { getUser, updateUser, loadConfig };Common mistakes when refactoring old Node.js code with Cursor
Why it's a problem: Forgetting to add async to the parent function when adding await
How to avoid: In your Cmd+K prompt, explicitly say 'add the async keyword to the parent function and all functions in the call chain that need it.'
Why it's a problem: Losing error codes and custom error properties during conversion
How to avoid: Add to your .cursorrules: 'Preserve all custom error properties (code, statusCode, details) when converting callbacks to async/await.'
Why it's a problem: Converting the function but not updating callers
How to avoid: Use @codebase in Cursor Chat to find all callers before starting the conversion. Refactor callers and the function together.
Best practices
- Commit to Git before every Composer Agent refactoring session
- Convert one file at a time and run tests between each conversion
- Use util.promisify() for Node.js core APIs instead of manual Promise wrapping
- Keep function names identical during conversion to minimize caller changes
- Add JSDoc @returns {Promise<T>} annotations to all converted functions
- Use Plan Mode (Shift+Tab) to have Cursor outline the migration plan before executing
- Start a new Cursor Chat session if the AI starts mixing callback and async patterns
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Node.js service file with callback-based functions for database operations. Convert all functions to async/await. Preserve error handling, keep function names the same, use util.promisify for fs operations, and add JSDoc @returns annotations. Show before and after for each function.
In Cursor Composer (Cmd+I): @src/services/userService.js Refactor all callback-based functions to async/await. Use try/catch for error handling. Use util.promisify for Node.js core APIs. Keep function names the same. Update callers within this file. Add JSDoc @returns {Promise<T>} to each function.
Frequently asked questions
Should I convert to raw Promises or async/await?
Always prefer async/await. It produces more readable code and Cursor generates better results with it. Add a .cursorrules directive to enforce async/await over .then() chains.
Can Cursor handle converting deeply nested callbacks?
Cursor handles two to three levels of nesting well. For deeper callback hell (4+ levels), break the refactoring into smaller steps: first extract inner callbacks into named functions, then convert each to async/await.
What if my project still needs to support callback-style callers?
You can ask Cursor to create a backward-compatible wrapper: keep the async version as the primary implementation and add a wrapper function that accepts a callback parameter using the pattern fn(...args, callback) { asyncFn(...args).then(r => callback(null, r)).catch(callback) }.
Will Cursor correctly handle error-first callbacks?
Usually yes. Cursor understands the Node.js error-first callback convention (err, result). Make sure your prompt mentions 'error-first callback pattern' if Cursor misidentifies the pattern.
How do I handle callback-based event emitters?
Event emitters should not be converted to async/await since they emit multiple events over time. Tell Cursor to skip EventEmitter patterns and only convert single-completion callbacks.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation