Callbacks and Promises in JavaScript
Callbacks and Promises in JavaScript
~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
With the event loop knowledge from chapter 23, let's now build our budget app's first REAL asynchronous operation: loading transactions from a file.
The classic callback pattern
Before promises, the only way to react to an asynchronous result was to pass a callback function that gets CALLED once the result is available:
import { readFile } from 'node:fs';
readFile('transactions.json', 'utf-8', (error, content) => {
if (error) {
console.error('Error reading file:', error.message);
return;
}
console.log('Content:', content);
});
console.log('This line runs BEFORE the file content!'); // the chapter 23 call stack in actionCallback hell: the problem with nested callbacks
Once several asynchronous steps need to run ONE AFTER ANOTHER, the callback pattern leads to deeply nested, hard-to-read code:
loadTransactions((error, transactions) => {
if (error) return console.error(error);
calculateBalance(transactions, (error, balance) => {
if (error) return console.error(error);
saveBalance(balance, (error) => {
if (error) return console.error(error);
console.log('Done! Balance saved:', balance);
// ... and so on, indented further and further to the right
});
});
});Achtung: This "arrowhead" pattern (the "pyramid of doom") is the main reason promises were introduced – they solve exactly this nesting problem.
Promises: a promise of a future value
A promise represents a value that isn't available NOW, but will be SOMETIME. A promise is always in exactly one of three states: pending, fulfilled, or rejected.
function loadTransactionsPromise() {
return new Promise((resolve, reject) => {
readFile('transactions.json', 'utf-8', (error, content) => {
if (error) {
reject(error); // the promise becomes 'rejected'
} else {
resolve(JSON.parse(content)); // the promise becomes 'fulfilled' with this value
}
});
});
}
loadTransactionsPromise()
.then(transactions => console.log('Loaded:', transactions.length))
.catch(error => console.error('Error:', error.message));Chaining promises: the end of callback hell
.then() itself returns another promise – this lets several asynchronous steps line up FLAT instead of nested, the identical example from above, now readable:
loadTransactionsPromise()
.then(transactions => calculateBalancePromise(transactions))
.then(balance => saveBalancePromise(balance))
.then(() => console.log('Done!'))
.catch(error => console.error('Some step failed:', error.message));Several promises in parallel: Promise.all
When several INDEPENDENT asynchronous operations should run IN PARALLEL (not sequentially), Promise.all() is the right tool:
Promise.all([
loadTransactionsPromise('january.json'),
loadTransactionsPromise('february.json'),
loadTransactionsPromise('march.json'),
])
.then(([january, february, march]) => {
console.log('All three months loaded:', january.length + february.length + march.length);
})
.catch(error => console.error('At least one of the three failed:', error.message));Achtung: Promise.all() fails IMMEDIATELY and entirely as soon as EVEN ONE of the given promises rejects ("fail fast"). If ALL results should be collected regardless of whether individual ones fail, Promise.allSettled() is the better alternative.
Promises are already considerably more readable than nested callbacks – in chapter 25, we'll learn async/await, an even more elegant syntax for exactly the same concept.