Skip to content
Prompt of the Day — Part 20 of 30

Prompt of the Day: Convert Callback-Based Code to Async/Await

Written by claude-sonnet-4 · Edited by claude-sonnet-4
async-awaitcallbacksjavascriptnodejspythonrefactoringlegacy-codepromisesprompt-of-the-day

Part 20 of 30 — Prompt of the Day Series


The 11 PM War Story

A few years back I was brought in to help a team shipping a Node.js payment processing service. The codebase had been written circa 2014 — back when callbacks were the way to handle async operations in JavaScript. By the time I arrived, the file that handled order confirmation was 400 lines of pyramid-of-doom: fs.readFile inside a database query inside an HTTP call inside another HTTP call, each failure path handled (or more often, silently swallowed) by a separate if (err) branch scattered across six levels of indentation.

The lead dev handed me a printout — an actual printout — with arrows drawn between the callbacks because the screen wasn't wide enough to see the nesting. That's callback hell in physical form.

Modern AI coding assistants can untangle this mess in seconds. But only if you give them the right prompt.


The Prompt

Convert the following [JavaScript/Node.js/Python] callback-based function to use async/await.

Requirements:
1. Preserve all existing behavior and error handling — do not silently drop any error paths.
2. Wrap the innermost callback-based calls with util.promisify (Node.js) or asyncio equivalents (Python) rather than rewriting third-party library internals.
3. Replace nested callbacks with sequential awaits for dependent operations and Promise.all() for operations that can run in parallel.
4. Add a single top-level try/catch block that re-throws errors with the original error message intact.
5. Do not change the function signature or return type.

Here is the function:
[PASTE YOUR CODE]

Why It Works

This prompt does five things that generic "convert to async/await" prompts miss:

It anchors behavior, not just syntax. The biggest risk in a callback-to-async migration is silently dropping an error path. Callback code often has asymmetric error handling — some branches call callback(err), others just return. By explicitly requiring all error paths to be preserved, you force the model to audit each branch rather than pattern-matching the structure.

It specifies the right conversion tool. In Node.js, util.promisify is the correct idiom for wrapping standard callback functions (anything that follows the (err, result) convention). OneUptime's 2026 deep dive on Node.js callback modernization shows that util.promisify with .bind() is the safest path for class methods and avoids a common this-context bug that manual Promise wrappers introduce. Telling the AI which tool to use means you get idiomatic, maintainable output instead of home-rolled wrapper functions.

It distinguishes sequential vs. parallel. Without this instruction, most AI assistants will convert callback(a, () => callback(b, ...)) into a chain of sequential await a; await b; — which is functionally correct but 2x slower when a and b don't depend on each other. Calling out Promise.all() for parallel paths gets you performance gains for free.

It preserves the function signature. AI models love to refactor. Unconstrained, they'll rename parameters, change return types, and wrap the whole thing in a class. Adding "do not change the function signature" keeps the migration surgical.

It mandates a single try/catch. IN-COM's 2025 guide to escaping callback hell identifies centralized error handling as the primary practical benefit of async/await over callbacks. One try/catch that re-throws with the original message gives upstream callers the error context they need without you scattering error checks across the function body.


A Real Conversion: Before and After

Before (Callback Hell — Node.js)

function processOrder(orderId, callback) {
  db.query('SELECT * FROM orders WHERE id = ?', [orderId], function(err, order) {
    if (err) return callback(err);
    if (!order) return callback(new Error('Order not found'));

    inventory.check(order.productId, function(err, inStock) {
      if (err) return callback(err);
      if (!inStock) return callback(new Error('Out of stock'));

      payment.charge(order.customerId, order.amount, function(err, receipt) {
        if (err) return callback(err);

        db.query('UPDATE orders SET status = ? WHERE id = ?',
          ['paid', orderId],
          function(err) {
            if (err) return callback(err);
            email.send(order.customerEmail, receipt, function(err) {
              if (err) return callback(err); // silently swallowed in the original
              callback(null, receipt);
            });
          }
        );
      });
    });
  });
}

After (Async/Await)

const { promisify } = require('util');

const dbQuery = promisify(db.query).bind(db);
const inventoryCheck = promisify(inventory.check).bind(inventory);
const paymentCharge = promisify(payment.charge).bind(payment);
const emailSend = promisify(email.send).bind(email);

async function processOrder(orderId) {
  try {
    const order = await dbQuery('SELECT * FROM orders WHERE id = ?', [orderId]);
    if (!order) throw new Error('Order not found');

    const inStock = await inventoryCheck(order.productId);
    if (!inStock) throw new Error('Out of stock');

    const receipt = await paymentCharge(order.customerId, order.amount);

    await dbQuery('UPDATE orders SET status = ? WHERE id = ?', ['paid', orderId]);
    await emailSend(order.customerEmail, receipt);

    return receipt;
  } catch (err) {
    throw err; // re-throw with original message intact
  }
}

The logic is identical. The readability is night-and-day. And unlike the original, every error path now bubbles up correctly.


The Anti-Prompt

Convert this code to async/await.

[PASTE CODE]

Why it fails: This prompt leaves every important decision to the model. In practice, you'll get a response that:

  • Converts sequential and parallel operations to sequential await chains (a known performance pitfall highlighted in multiple 2024–2025 async/await guides)
  • Drops subtle error paths that existed only as if (err) return callback(err) branches
  • Adds .then() chains next to await calls, mixing styles that confuse the next developer
  • Changes the function to return undefined instead of the original value because the model forgot to add return

The anti-prompt treats async/await conversion as a purely syntactic transformation. It isn't. It is a behavior-preserving refactor, and it requires explicit constraints.


Python Variation

For Python codebases migrating from threading or raw callbacks to asyncio:

Convert the following Python function from synchronous/threading-based I/O to async/await using asyncio.

Requirements:
1. Replace blocking I/O (requests, open, time.sleep) with their async equivalents (httpx.AsyncClient, aiofiles, asyncio.sleep).
2. Use asyncio.gather() for operations that can run concurrently.
3. Preserve all exception types and messages — do not convert exceptions to generic RuntimeError.
4. Add a single try/except block at the function boundary.
5. Do not change the function's public interface.

Here is the function:
[PASTE YOUR CODE]

Variations

For legacy fs callbacks in Node.js 14+: Add "Prefer the fs/promises API (require('fs/promises')) over util.promisify where the built-in promise API exists" — this eliminates wrapper boilerplate entirely for file operations.

For class methods: Add "Ensure promisified methods are bound to the class instance to preserve the correct 'this' context" — this catches the most common bug in class-based Node.js refactors.

For large files: Add "Convert one function at a time and leave the rest unchanged so the PR diff stays reviewable" — incremental migration is almost always safer than a full-file rewrite.

For TypeScript: Add "Update the return type annotation to Promise<OriginalReturnType> and add the async keyword to the type declaration" — keeps your types in sync with your implementation.


Action Items

  • Find one callback-heavy function in your codebase (search for function(err, or cb) patterns in Node.js projects)
  • Run it through today's prompt before writing a single line manually
  • After conversion, check: does every original if (err) return callback(err) have a corresponding path that throws or re-throws in the async version?
  • If the function calls multiple independent async operations sequentially, ask the AI to refactor those to Promise.all() in a follow-up prompt
  • Run your existing tests — if the behavior is preserved, the refactor is safe

Ask The Guild

What is the hairiest callback-based function you have ever had to modernize — and what was the gotcha that almost sunk you? Share your war story (and ideally the before/after diff) in the community thread. Bonus points if it involved this context loss on a promisified class method.

Copy A Prompt Next

Review and debug

If this article changed how you think about the problem, copy a prompt that turns that judgment into one safe, reviewable next step.

Matching public prompts

23

Keep the task scoped, copy the prompt, then inspect one reviewable diff before the agent continues.

Need the safest first move instead? Open the curated sample prompts before you browse the broader library.

Working With AI ToolsWorking With AI Tools

System Prompts — .cursorrules and CLAUDE.md Explained

Write system prompts that give AI persistent context about your project and preferences.

Preview
**Use this when you want the agent to draft your persistent project instructions:**
"Help me write a system prompt file for this project.
Tool target: [Cursor / Claude Code / both]
Project summary: [what the app does]
Stack: [frameworks, languages, key services]
Prompt Engineering

Turn this workflow advice into a durable operating system

Prompt and workflow posts are the quick win. The learning paths turn them into a durable operating model for tools, prompts, and agent supervision.

Best Next Path

Working With AI Tools

Explorer · Free

Turn ad hoc prompting into a repeatable workflow with better tool choice, stronger prompting, and safer day-to-day AI habits.

23 lessonsIncluded in the free Explorer plan

Need the free route first?

Start with Foundations for AI-Assisted Builders if you want the workflow and vocabulary before you dive into the deeper path above.

T

About Tom Hundley

Tom Hundley writes for builders who need stronger technical judgment around AI-assisted software work. The Guild turns production experience into public articles, copy-paste prompts, and structured learning paths that help non-software developers supervise AI agents more safely.

Do this next

Leave this article with one concrete move. Copy the matching prompt, or start with the path that teaches the safest next skill in sequence.