Skip to content

From Callback Hell to Async Heaven

A Love Letter to setTimeout (but it’s time to move on)

Section titled “A Love Letter to setTimeout (but it’s time to move on)”

If you’re like many devs (me included, once), your journey into asynchronous JavaScript probably started with a setTimeout() and a dream.

setTimeout(() => {
console.log("Do something after 1 second...");
}, 1000);

It’s simple. It works. But what happens when you need several steps, in order? This happens:

setTimeout(() => {
console.log("Step 1");
setTimeout(() => {
console.log("Step 2");
setTimeout(() => {
console.log("Step 3");
}, 1000);
}, 1000);
}, 1000);

It becomes unreadable, hard to debug, and worse—hard to reuse.

new Promise((resolve, reject) => {
// async logic here
});

A basic wait() function looks like this:

function wait(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}```
```js
wait(1000)
.then(() => {
console.log("Step 1");
return wait(1000); // return new Promise
})
.then(() => {
console.log("Step 2");
return wait(1000);
})
.then(() => {
console.log("Step 3");
});

No more deeply nested callbacks—but still a bit verbose.

✨ Step 2: async/await — Cleaner, More Sane

Section titled “✨ Step 2: async/await — Cleaner, More Sane”

You can use async/await as a cleaner syntax on top of Promises.

function wait(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
async function runSteps() {
await wait(1000);
console.log("Step 1");
await wait(1000);
console.log("Step 2");
await wait(1000);
console.log("Step 3");
}
runSteps();

This code reads like synchronous code but is still non-blocking.

Whether you’re:

  • Fetching from an API
  • Waiting for an animation to finish
  • Responding to a user event
  • Running a queue of tasks in order

You should be using async/await for clarity, error handling, and future you.

🔁 Bonus: Mixing return new Promise() with async functions

Section titled “🔁 Bonus: Mixing return new Promise() with async functions”
function delayAndReturnValue(ms, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, ms);
});
}
async function doSomething() {
const result = await delayAndReturnValue(1000, 'Done!');
console.log(result); // "Done!" after 1 second
}
ConceptSyntax ExampleWhen to Use
CallbacksetTimeout(() => {...}, 1000)Simple, one-off delayed actions
Callback HellNested callbacksAvoid—hard to manage
Promisereturn new Promise((res, rej) => {})When modernizing callback-based logic
Chained Promise.then().then()When awaiting sequential tasks
async/awaitawait wait()Default for modern async workflows

You can start small. Take one function, wrap it in a Promise, then call it from an async function.

If you love setTimeout (and hey, we all do), just know: async/await doesn’t take it away—it just gives it a cleaner, saner structure.