All three of these solve the same underlying problem: running code once a slow operation finishes, without freezing everything else while it waits. By 2026 you will meet all three in real codebases, often in the same file. Callbacks are the oldest and still the foundation everything else is built on. Promises wrap a callback in an object you can chain and pass around. Async await is syntax that lets you write promise-based code as if it were synchronous. None of them is obsolete; each one is still the right tool somewhere.
What changed in 2026
- Nearly every core API now has a promise-based version. Node.js and browser APIs that used to be callback-only mostly ship a promise-based alternative too, such as
fs.promises, so writing new callback-based I/O code in 2026 is a choice, not a requirement.
- Default linting nudges teams toward one style. Most 2026 starter kits flag unhandled promise rejections and favor async await over raw
.then() chains.
- AI-generated code defaults to async await almost universally. Callback-based code in a codebase is now more often a sign of age, or of wrapping a callback-only library, than a recent stylistic choice.
The same task, three ways
// 1. Callback
function loadUser(id, callback) {
fetchUserFromApi(id, (err, user) => {
if (err) return callback(err);
callback(null, user);
});
}
// 2. Promise
function loadUser(id) {
return fetchUserFromApi(id)
.then(user => user)
.catch(err => { throw err; });
}
// 3. Async await
async function loadUser(id) {
try {
return await fetchUserFromApi(id);
} catch (err) {
throw err;
}
}
Same behavior, three shapes. The callback version needs a manual error check on every single call. The promise version centralizes errors in .catch. The async await version reads closest to ordinary synchronous code.
Side-by-side comparison
|
Callbacks |
Promises |
Async await |
| Introduced |
Always been in JavaScript |
ES2015 |
ES2017 |
| Error handling |
Manual check on every call |
.catch() |
try / catch |
| Reads like synchronous code |
No |
Somewhat, when chained |
Yes |
| Running tasks in parallel |
Manual counting or a library |
Promise.all |
await Promise.all |
| Risk of deep nesting |
High, sometimes called callback hell |
Low |
Very low |
| Underlying mechanism |
Function passed as an argument |
Object wrapping a callback |
Sugar over promises |
How they relate to each other
Async await is not a competitor to promises; it is built directly on top of them. An async function always returns a promise, and await unwraps one. Promises, in turn, are typically implemented by wrapping a plain callback-based operation, such as a timer or a network request, once at the boundary, so the rest of the code never has to touch a raw callback again. This is why converting an old callback API usually means writing one small wrapper rather than rewriting everything downstream. That wrapper function holds onto resolve and reject between calls using a closure, the same mechanism that makes private variables work.
Running things in parallel, three ways
// Callback: manual counting
let done = 0, results = [];
ids.forEach((id, i) => {
fetchUserFromApi(id, (err, user) => {
results[i] = user;
if (++done === ids.length) allDone(results);
});
});
// Promise
Promise.all(ids.map(id => fetchUserFromApi(id))).then(allDone);
// Async await
const results = await Promise.all(ids.map(id => fetchUserFromApi(id)));
This is where the gap is widest. The callback version is easy to get wrong, miscounting done is a classic bug, while both promise-based versions get parallelism for free from Promise.all.
When to pick which
Writing new code today, default to async await; it is the most readable and the current convention. Combining several independent async operations, use Promise.all or Promise.allSettled. Wrapping an old callback-only library, write one small promise wrapper around it, then use async await everywhere else that calls it. The underlying scheduling stays identical no matter which style sits on top; see what the event loop is. Inheriting a large callback-based codebase, avoid a big rewrite; wrap and convert file by file as each one is touched anyway.
FAQ
Is async await faster than promises or callbacks?
No. It is the same underlying mechanism as promises, which are the same underlying mechanism as callbacks. The differences are readability and error handling, not raw performance.
Do I still need to learn callbacks if async await exists?
Yes. Many libraries and browser APIs, event listeners, timers, and streams among them, are still callback-based at their core, and promises themselves are usually built by wrapping one.
Can I mix async await and then chains in the same codebase?
Yes, they are fully compatible since async await is built on promises, but pick one style per function so the code stays easy to follow.
What is callback hell, and does async await fix it?
Callback hell is deeply nested callbacks that result from chaining dependent async steps. Async await effectively eliminates it for sequential code, since each step becomes the next line instead of a new nested function.
Where to go next