How do you escape callback hell?
You convert the callback-based functions into ones that return promises, and then you write the sequence as a flat chain or as an async function with awaits. The nesting disappears because each step now produces a value you can pass to the next one.
// The promise version of a two step sequence.
const readFileAsync = (name) =>
new Promise((resolve) => setTimeout(() => resolve("content of " + name), 5));
const parseAsync = (text) =>
new Promise((resolve) => setTimeout(() => resolve({ lines: text.length }), 5));
async function run() {
const text = await readFileAsync("notes.txt");
const parsed = await parseAsync(text);
console.log(parsed.lines);
}
run();The change is small and mechanical. A callback function becomes a function that returns a promise, and the call site changes from passing a callback to using await. The steps keep their order, but they sit next to each other rather than nested inside each other.
Two techniques are worth naming. The first is a generic promisify helper, which turns any function that follows the error-first callback convention into one that returns a promise. Write it once and every legacy API in your codebase becomes chainable. The second is to wrap a whole callback-based module at its boundary, so the rest of your code only ever sees promises.
There is a middle option too. Some teams keep the callback API but give each step a named function and pass the next step in explicitly, which is the continuation passing style. It flattens the structure without changing the underlying contract, and it is a reasonable choice when you cannot change the API you are calling.