Hi! I'm Gaurang. I’ve worked extensively with Node.js in the past, and during that time, I spent a lot of effort wrangling with different async patterns — callbacks, promises, and async/await. This article is part of my series, My guide to getting through the maze of callbacks, promises, and async-await, where I share lessons learned and best practices for writing asynchronous code in Node.js.
This article assumes you’re familiar with basic Node.js concepts, especially asynchronous programming constructs like callbacks, promises, and async/await.
Table of Contents
Converting a Promise-based Function to Use Callbacks
Often, you’ll find yourself working in older codebases where asynchronous functions are written using the callback pattern. Node.js also offers a built-in way to convert a Promise-returning function into a callback-based one using util.callbackify()
.
Personally, I believe we should avoid writing new callbacks whenever possible. Even in a callback-heavy codebase, it's often worth introducing promises or async/await for better readability and maintainability.
I’ve spent years digging through callback-infested codebases, and I’ll say it plainly: callbacks are obsolete. They had their time, but that time is over. Stop writing them. Let them die.
Use this only when absolutely necessary — like when you're deep in a legacy callback jungle and need to drop in a Promise-based function without rewriting everything around it.
Prefer Then-chaining over nesting
It's common to see people misuse promises by nesting .then()
calls instead of chaining them. This leads to "Promise hell", which is structurally similar to "callback hell."
Callbacks are difficult to work with primarily because of their deeply nested structure. Promises were introduced to address this by enabling a more linear, manageable code flow.
Of course, code with then-chaining may not be as readable and intuitive as synchronous code or code written with async-await. But it is more manageable than code with nested thens.
For a deeper dive into why Promise hell is best avoided — and how to write cleaner promise chains — check out these excellent articles:

How to escape Promise Hell. Unlike Callback Hell, Promise Hell is… | by Ronald Chen | Medium
Ronald Chen ・ ・
Medium
Keep in mind that it’s not just .then()
chains you need to keep clean — don’t nest callback-based functions inside a .then()
either. This is a common mistake that defeats the purpose of using Promises in the first place.
If you’re already using Promises, don’t go back to callbacks mid-way. Promises are designed to give you a linear, manageable control flow — and the moment you reintroduce nested callbacks, you throw that benefit away. Whether you’re nesting callbacks or .then()
blocks, the result is the same: harder-to-read code.
Instead, convert the callback-based function into a Promise using any of the methods discussed in my previous article "Using callback-based functions when the rest of the code uses Promises" and keep your chain linear.
Notice that readFile2CB
is a callback-based function. If you nest it (as shown in the bad-code example), any code that relies on content2
or results
must go inside that nested block. Over time, this leads to deeply indented, harder-to-read code — exactly the kind of mess Promises were created to prevent.
Once you start using Promises, commit to the pattern. Nesting defeats the purpose. Whether you’re nesting .then()
blocks or stuffing callbacks inside them, you’re trading away clarity and maintainability — the very reasons Promises exist.
If you're stuck in a callback-heavy codebase, I feel your pain. Use these patterns sparingly, but push for modern async functions where you can — your future self (and your teammates) will thank you.
I've put a lot of work into this article. And I hope that it has been helpful. If you have any questions, please feel free to leave a comment below. I'd really appreciate it if you could like and share the article! 😊
Top comments (5)
Thanks for sharing this! A lot of people struggle with using promises, and it's even harder when we have to mix different paradigms.
There's nothing wrong with storing interim data without using
let
in a closure, but I find it opens up developers to lose track of thinking in asynchronous, and make mistakes like trying to access the variables outside of the promise chain.While the example may have "business logic" around why
readFile1Async
needs to happen beforereadFile2Async
, I want to present some options.I'm not suggesting that variables in a closure are a bad pattern, just offering some alternatives.
Totally agree! I’ve had a similar experience - using variables from outer scopes can really make the control flow difficult to follow. That’s probably one of the reasons why callback hell is so tough to manage. Thanks for highlighting this!
Growth like this is always nice to see. Kinda makes me wonder - what keeps stuff going long-term? Like, beyond just the early hype?
Great guide! Mixing promises into a callback-based codebase can be tricky — especially when managing control flow. Using util.promisify is a lifesaver for gradually modernizing legacy code.
Absolutely! I actually wrote a previous article that dives deeper into util.promisify: "[Node.js] Using callback-based functions when the rest of the code uses Promises". Would love to hear your thoughts on that too!