Skip to content

Commit 035fe03

Browse files
authored
Update and refactor async flow guide using async/await and better explanations
The previous version of the Asynchronous Flow Control guide relied on callback-based patterns which, while historically important, led to "callback hell" and are no longer considered best practice. Changes include: * Replacing the callback-hell example with a modern async/await section to introduce the reader to a better solution. * Rewrote all core examples (In Series, Parallel, Limited Parallel) to use async/await, Promise.all, and Promise.race. * Simplified the Beer Song problem explanation while maintaining the details intact. * Corrected minor mistakes that could result in a misunderstanding. Signed-off-by: Ahmed Emad <ahmed.emad.edu@gmail.com>
1 parent cc36077 commit 035fe03

File tree

1 file changed

+174
-110
lines changed

1 file changed

+174
-110
lines changed

apps/site/pages/en/learn/asynchronous-work/asynchronous-flow-control.md

Lines changed: 174 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ async1(function (input, result1) {
2020
async3(function (result3) {
2121
async4(function (result4) {
2222
async5(function (output) {
23-
// do something with output
23+
// Do something with output.
2424
});
2525
});
2626
});
@@ -30,7 +30,35 @@ async1(function (input, result1) {
3030

3131
Of course, in real life there would most likely be additional lines of code to handle `result1`, `result2`, etc., thus, the length and complexity of this issue usually results in code that looks much more messy than the example above.
3232

33-
**This is where _functions_ come in to great use. More complex operations are made up of many functions:**
33+
## The Modern Solution: [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
34+
To solve callback hell, modern JavaScript introduced async and await. This is syntactic sugar built on top of a concept called Promises, allowing you to write asynchronous code that looks and behaves like synchronous code. It makes complex flows much easier to reason about.
35+
36+
* An async function is a function that implicitly returns a Promise.
37+
38+
* The await keyword can only be used inside an async function. It pauses the function's execution and waits for a Promise to be resolved, then resumes with the resolved value.
39+
40+
41+
Let's rewrite the "callback hell" example using async/await. Notice how flat and readable it becomes:
42+
```js
43+
async function performOperations() {
44+
try {
45+
const result1 = await async1(input);
46+
const result2 = await async2(result1);
47+
const result3 = await async3(result2);
48+
const result4 = await async4(result3);
49+
const output = await async5(result4);
50+
// Do something with output.
51+
} catch (error) {
52+
// Handle any error that occurred in the chain.
53+
console.error(error);
54+
}
55+
}
56+
```
57+
This code is executed sequentially from top to bottom, just like synchronous code, but without blocking the main thread.
58+
59+
**Thinking in Functions**
60+
61+
Even with async/await, it's useful to structure complex operations as a series of distinct functions. This improves modularity and reusability. A common pattern involves:
3462

3563
1. initiator style / input
3664
2. middleware
@@ -46,24 +74,26 @@ Network requests can be incoming requests initiated by a foreign network, by ano
4674

4775
A middleware function will return another function, and a terminator function will invoke the callback. The following illustrates the flow to network or file system requests. Here the latency is 0 because all these values are available in memory.
4876

77+
Here’s how we can structure a simple flow using async functions. Each function passes its result to the next.
4978
```js
50-
function final(someInput, callback) {
51-
callback(`${someInput} and terminated by executing callback `);
79+
async function final(someInput) {
80+
return `${someInput} and terminated.`;
5281
}
5382

54-
function middleware(someInput, callback) {
55-
return final(`${someInput} touched by middleware `, callback);
83+
async function middleware(someInput) {
84+
const processedInput = `${someInput}touched by middleware, `;
85+
return await final(processedInput);
5686
}
5787

58-
function initiate() {
59-
const someInput = 'hello this is a function ';
60-
middleware(someInput, function (result) {
61-
console.log(result);
62-
// requires callback to `return` result
63-
});
88+
async function initiate() {
89+
const someInput = 'hello this is a function, ';
90+
// We await the result of the entire chain.
91+
const result = await middleware(someInput);
92+
console.log(result);
6493
}
6594

6695
initiate();
96+
// Output: hello this is a function, touched by middleware, and terminated.
6797
```
6898

6999
## State management
@@ -86,9 +116,7 @@ function getSong() {
86116
let _song = '';
87117
let i = 100;
88118
for (i; i > 0; i -= 1) {
89-
_song += `${i} beers on the wall, you take one down and pass it around, ${
90-
i - 1
91-
} bottles of beer on the wall\n`;
119+
_song += `${i} beers on the wall, you take one down and pass it around, ${i - 1} bottles of beer on the wall\n`;
92120
if (i === 1) {
93121
_song += "Hey let's get some more beer";
94122
}
@@ -115,9 +143,7 @@ function getSong() {
115143
let i = 100;
116144
for (i; i > 0; i -= 1) {
117145
setTimeout(function () {
118-
_song += `${i} beers on the wall, you take one down and pass it around, ${
119-
i - 1
120-
} bottles of beer on the wall\n`;
146+
_song += `${i} beers on the wall, you take one down and pass it around, ${i - 1} bottles of beer on the wall\n`;
121147
if (i === 1) {
122148
_song += "Hey let's get some more beer";
123149
}
@@ -138,132 +164,170 @@ singSong(song);
138164
// Uncaught Error: song is '' empty, FEED ME A SONG!
139165
```
140166

141-
Why did this happen? `setTimeout` instructs the CPU to store the instructions elsewhere on the bus, and instructs that the data is scheduled for pickup at a later time. Thousands of CPU cycles pass before the function hits again at the 0 millisecond mark, the CPU fetches the instructions from the bus and executes them. The only problem is that song ('') was returned thousands of cycles prior.
167+
Why did this happen? setTimeout (like file system or network requests) instructs the Node.js event loop to schedule the provided function for execution at a later time. The for loop completes almost instantly, and _song (which is still an empty string) is returned immediately. The functions scheduled by setTimeout run much later, long after singSong has attempted to use the empty _song.
142168

143-
The same situation arises in dealing with file systems and network requests. The main thread simply cannot be blocked for an indeterminate period of time-- therefore, we use callbacks to schedule the execution of code in time in a controlled manner.
169+
The main thread cannot be blocked indefinitely while waiting for I/O or other asynchronous tasks. Fortunately, Promises and async/await provide the mechanisms to explicitly wait for these operations to complete before continuing, allowing us to manage asynchronous control flow effectively.
144170

145-
You will be able to perform almost all of your operations with the following 3 patterns:
171+
You will be able to perform almost all of your operations with the following 3 patterns using async/await:
146172

147173
1. **In series:** functions will be executed in a strict sequential order, this one is most similar to `for` loops.
148174

149175
```js
150-
// operations defined elsewhere and ready to execute
176+
// Simulate an asynchronous operation and return a Promise.
177+
const simulateAsyncOp = (id, durationMs) => new Promise(resolve => {
178+
console.log(`[${id}] Starting operation.`);
179+
setTimeout(() => {
180+
console.log(`[${id}] Finished operation.`);
181+
resolve(`Operation ${id} complete.`);
182+
}, durationMs);
183+
});
184+
151185
const operations = [
152-
{ func: function1, args: args1 },
153-
{ func: function2, args: args2 },
154-
{ func: function3, args: args3 },
186+
() => simulateAsyncOp(1, 500),
187+
() => simulateAsyncOp(2, 200),
188+
() => simulateAsyncOp(3, 300),
155189
];
156190

157-
function executeFunctionWithArgs(operation, callback) {
158-
// executes function
159-
const { args, func } = operation;
160-
func(args, callback);
191+
// Executes an array of asynchronous functions in series.
192+
async function executeInSeries(asyncFunctions) {
193+
console.log("\n--- Starting In Series Execution ---");
194+
for (const fn of asyncFunctions) {
195+
const result = await fn(); // 'await' pauses here until the Promise resolves.
196+
console.log(` Result: ${result}`);
197+
}
198+
console.log("--- All In Series operations completed ---");
161199
}
162200

163-
function serialProcedure(operation) {
164-
if (!operation) process.exit(0); // finished
165-
executeFunctionWithArgs(operation, function (result) {
166-
// continue AFTER callback
167-
serialProcedure(operations.shift());
168-
});
201+
(async () => {
202+
await executeInSeries(operations);
203+
})();
204+
```
205+
**Applying "In Series": The Beer Song Solution:** The "In Series" pattern is precisely what's needed to fix the song generation, it makes sure that each line is created then added in the correct order.
206+
207+
```js
208+
async function getSong() {
209+
const _songParts = [];
210+
for (let i = 100; i > 0; i -= 1) {
211+
// Await for each line.
212+
const line = await new Promise(resolve => {
213+
setTimeout(() => {
214+
let currentLine = `${i} beers on the wall, you take one down and pass it around, ${
215+
i - 1
216+
} bottles of beer on the wall\n`;
217+
if (i === 1) {
218+
currentLine += "Hey let's get some more beer";
219+
}
220+
resolve(currentLine);
221+
}, 0);
222+
});
223+
_songParts.push(line);
224+
}
225+
return _songParts.join('');
169226
}
170227

171-
serialProcedure(operations.shift());
228+
function singSong(songContent) {
229+
if (!songContent) throw new Error("Song is empty, cannot sing!");
230+
console.log("\n--- Singing the Song ---");
231+
console.log(songContent);
232+
console.log("--- Song Finished ---");
233+
}
234+
235+
(async () => {
236+
const fullSong = await getSong();
237+
singSong(fullSong);
238+
})();
172239
```
173240

174241
2. **Limited in series:** functions will be executed in a strict sequential order, but with a limit on the number of executions. Useful when you need to process a large list but with a cap on the number of items successfully processed.
175242

176243
```js
177-
let successCount = 0;
244+
// Simulate an asynchronous task.
245+
const processItem = (id) => new Promise(resolve => {
246+
const delay = Math.random() * 500 + 50;
247+
console.log(`[Item ${id}] Starting.`);
248+
setTimeout(() => {
249+
console.log(`[Item ${id}] Finished.`);
250+
resolve(`Item ${id} processed.`);
251+
}, delay);
252+
});
178253

179-
function final() {
180-
console.log(`dispatched ${successCount} emails`);
181-
console.log('finished');
182-
}
254+
// An array of samples
255+
const itemsToProcess = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
183256

184-
function dispatch(recipient, callback) {
185-
// `sendMail` is a hypothetical SMTP client
186-
sendMail(
187-
{
188-
subject: 'Dinner tonight',
189-
message: 'We have lots of cabbage on the plate. You coming?',
190-
smtp: recipient.email,
191-
},
192-
callback
193-
);
194-
}
257+
// Processes items within a limit.
258+
async function processLimitedInSeries(items, limit) {
259+
const queue = [...items];
260+
const active = new Set(); // Tracks currently running promises.
195261

196-
function sendOneMillionEmailsOnly() {
197-
getListOfTenMillionGreatEmails(function (err, bigList) {
198-
if (err) throw err;
262+
console.log(`\n--- Starting Limited In Series Execution (Limit: ${limit}) ---`);
199263

200-
function serial(recipient) {
201-
if (!recipient || successCount >= 1000000) return final();
202-
dispatch(recipient, function (_err) {
203-
if (!_err) successCount += 1;
204-
serial(bigList.pop());
205-
});
264+
while (queue.length > 0 || active.size > 0) {
265+
while (active.size < limit && queue.length > 0) {
266+
const item = queue.shift();
267+
const promise = processItem(item);
268+
active.add(promise);
269+
promise.finally(() => active.delete(promise)); // Remove the Promise from active when done.
206270
}
207271

208-
serial(bigList.pop());
209-
});
272+
// If all tasks are done or none active to wait for, break.
273+
if (active.size === 0 && queue.length === 0) break;
274+
275+
// Wait for at least one active promise to finish.
276+
if (active.size > 0) {
277+
await Promise.race(active);
278+
}
279+
}
280+
console.log("--- All Limited In Series operations completed ---");
210281
}
211282

212-
sendOneMillionEmailsOnly();
283+
(async () => {
284+
await processLimitedInSeries(itemsToProcess, 3); // Process 3 items at a time.
285+
})();
213286
```
214287

215-
3. **Full parallel:** when ordering is not an issue, such as emailing a list of 1,000,000 email recipients.
288+
3. **Full parallel:** when ordering is not an issue, such as firing 5 tasks at a time.
289+
290+
> The name here is a bit misleading because the tasks are fired sequentially and are being handled concurrently, therefore, it is not in parallel.
216291
217292
```js
218-
let count = 0;
219-
let success = 0;
220-
const failed = [];
221-
const recipients = [
222-
{ name: 'Bart', email: 'bart@tld' },
223-
{ name: 'Marge', email: 'marge@tld' },
224-
{ name: 'Homer', email: 'homer@tld' },
225-
{ name: 'Lisa', email: 'lisa@tld' },
226-
{ name: 'Maggie', email: 'maggie@tld' },
227-
];
293+
// A function that returns takes an id and returns a promise.
294+
const processItem = (id) =>
295+
new Promise((resolve) => {
296+
const delay = Math.random() * 500 + 50;
297+
console.log(`[Item ${id}] Starting.`);
298+
setTimeout(() => {
299+
console.log(`[Item ${id}] Finished.`);
300+
resolve(`Item ${id} processed.`);
301+
}, delay);
302+
});
228303

229-
function dispatch(recipient, callback) {
230-
// `sendMail` is a hypothetical SMTP client
231-
sendMail(
232-
{
233-
subject: 'Dinner tonight',
234-
message: 'We have lots of cabbage on the plate. You coming?',
235-
smtp: recipient.email,
236-
},
237-
callback
238-
);
239-
}
304+
// A array of samples.
305+
const itemsToProcess = [1, 2, 3, 4, 5];
240306

241-
function final(result) {
242-
console.log(`Result: ${result.count} attempts \
243-
& ${result.success} succeeded emails`);
244-
if (result.failed.length)
245-
console.log(`Failed to send to: \
246-
\n${result.failed.join('\n')}\n`);
247-
}
307+
async function processInParallel(items) {
308+
console.log("\n--- Starting Full Parallel Execution ---");
248309

249-
recipients.forEach(function (recipient) {
250-
dispatch(recipient, function (err) {
251-
if (!err) {
252-
success += 1;
253-
} else {
254-
failed.push(recipient.name);
255-
}
256-
count += 1;
310+
// Creating an array of promises using processItem().
311+
const promises = items.map((item) => processItem(item));
257312

258-
if (count === recipients.length) {
259-
final({
260-
count,
261-
success,
262-
failed,
263-
});
264-
}
265-
});
266-
});
313+
// Await for all the promises to finish.
314+
const results = await Promise.all(promises);
315+
316+
console.log("--- All Full Parallel operations completed ---");
317+
console.log("Results:", results);
318+
}
319+
// Create an async function to await for processInParallel() aside.
320+
(async () => {
321+
await processInParallel(itemsToProcess);
322+
})();
267323
```
268324

269325
Each has its own use cases, benefits, and issues you can experiment and read about in more detail. Most importantly, remember to modularize your operations and use callbacks! If you feel any doubt, treat everything as if it were middleware!
326+
327+
### Choosing the Right Pattern
328+
329+
* **In Series:** Use when order is critical, or when each task depends on the previous one (e.g., chained database operations).
330+
331+
* **Limited in Series:** Use when you need concurrency but must control resource load or adhere to external rate limits (e.g., API calls to a throttled service).
332+
333+
* **Full Parallel:** Use when operations are independent and can run simultaneously for maximum throughput (e.g., fetching data from multiple unrelated sources).

0 commit comments

Comments
 (0)