You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
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.
32
32
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
+
asyncfunctionperformOperations() {
44
+
try {
45
+
constresult1=awaitasync1(input);
46
+
constresult2=awaitasync2(result1);
47
+
constresult3=awaitasync3(result2);
48
+
constresult4=awaitasync4(result3);
49
+
constoutput=awaitasync5(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:
34
62
35
63
1. initiator style / input
36
64
2. middleware
@@ -46,24 +74,26 @@ Network requests can be incoming requests initiated by a foreign network, by ano
46
74
47
75
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.
48
76
77
+
Here’s how we can structure a simple flow using async functions. Each function passes its result to the next.
49
78
```js
50
-
functionfinal(someInput, callback) {
51
-
callback(`${someInput} and terminated by executing callback `);
79
+
asyncfunctionfinal(someInput) {
80
+
return`${someInput} and terminated.`;
52
81
}
53
82
54
-
functionmiddleware(someInput, callback) {
55
-
returnfinal(`${someInput} touched by middleware `, callback);
83
+
asyncfunctionmiddleware(someInput) {
84
+
constprocessedInput=`${someInput}touched by middleware, `;
85
+
returnawaitfinal(processedInput);
56
86
}
57
87
58
-
functioninitiate() {
59
-
constsomeInput='hello this is a function ';
60
-
middleware(someInput, function (result) {
61
-
console.log(result);
62
-
// requires callback to `return` result
63
-
});
88
+
asyncfunctioninitiate() {
89
+
constsomeInput='hello this is a function, ';
90
+
// We await the result of the entire chain.
91
+
constresult=awaitmiddleware(someInput);
92
+
console.log(result);
64
93
}
65
94
66
95
initiate();
96
+
// Output: hello this is a function, touched by middleware, and terminated.
67
97
```
68
98
69
99
## State management
@@ -86,9 +116,7 @@ function getSong() {
86
116
let _song ='';
87
117
let i =100;
88
118
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`;
92
120
if (i ===1) {
93
121
_song +="Hey let's get some more beer";
94
122
}
@@ -115,9 +143,7 @@ function getSong() {
115
143
let i =100;
116
144
for (i; i >0; i -=1) {
117
145
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`;
121
147
if (i ===1) {
122
148
_song +="Hey let's get some more beer";
123
149
}
@@ -138,132 +164,170 @@ singSong(song);
138
164
// Uncaught Error: song is '' empty, FEED ME A SONG!
139
165
```
140
166
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.
142
168
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.
144
170
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:
146
172
147
173
1.**In series:** functions will be executed in a strict sequential order, this one is most similar to `for` loops.
148
174
149
175
```js
150
-
// operations defined elsewhere and ready to execute
176
+
// Simulate an asynchronous operation and return a Promise.
// Executes an array of asynchronous functions in series.
192
+
asyncfunctionexecuteInSeries(asyncFunctions) {
193
+
console.log("\n--- Starting In Series Execution ---");
194
+
for (constfnof asyncFunctions) {
195
+
constresult=awaitfn(); // 'await' pauses here until the Promise resolves.
196
+
console.log(` Result: ${result}`);
197
+
}
198
+
console.log("--- All In Series operations completed ---");
161
199
}
162
200
163
-
functionserialProcedure(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
+
awaitexecuteInSeries(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
+
asyncfunctiongetSong() {
209
+
const_songParts= [];
210
+
for (let i =100; i >0; i -=1) {
211
+
// Await for each line.
212
+
constline=awaitnewPromise(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('');
169
226
}
170
227
171
-
serialProcedure(operations.shift());
228
+
functionsingSong(songContent) {
229
+
if (!songContent) thrownewError("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
+
constfullSong=awaitgetSong();
237
+
singSong(fullSong);
238
+
})();
172
239
```
173
240
174
241
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.
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
+
awaitprocessInParallel(itemsToProcess);
322
+
})();
267
323
```
268
324
269
325
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