Asynchronous Javascript - DavidBK/shampoo GitHub Wiki
Estimation time: 1-4 Days
This learning path will teach you how to write asynchronous code in JavaScript and Node.js.
Learning objectives:
At the end of this learning path, you'll be able to:
- Write an efficient asynchronous code
- Use the
asyncandawaitkeywords - Write scalable and performant code
Send me back home
[[TOC]]
Learning note: all links in this path are reading tutorials. You can read them but you can watch a youtube crash course. if you find useful links, please share them with me.
Here is some examples links:
Asynchronous programming is a technique that enables your program to start a potentially long-running task and still be able to be responsive to other events while that task runs, rather than having to wait until that task has finished. Once that task has finished, your program is presented with the result.
Many functions provided by nodeJS and browsers can potentially take a long time and therefore, are asynchronous.
After each mini chapter in this path, create a "Pull Request" and/or talk to your mentor to validate your knowledge.
We will learn a several techniques to make our programs asynchronous. Some of these techniques are legacy and not recommended for new projects but they are still important to understand.
-
Read the following script:
// This function use setTimeout to simulate a long running task function logAfterMs(message, ms) { setTimeout(() => console.log(message), ms); } logAfterMs("1", 0); logAfterMs("2", 1); logAfterMs("3", 10); logAfterMs("4", 5); console.log("5");
What do you think will be the output?
-
Create a
intro.jsfile and run it in the terminal.node intro.js
What is the output? why?
A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of action or job.
Here is a example to synchronous use of callback function:
function greeting(name) {
console.log(`Hello, ${name}`);
}
function addUser(callback) {
const userName = "David";
console.log("Adding user...");
callback(userName);
}
addUser(greeting);Or in anonyms arrow function style:
addUser((name) => {
console.log(`Hello, ${name}`);
});The callback itself can have inside it another callback function:
addUser((name) => {
console.log(`Hello, ${name}`);
addUser((name) => {
console.log(`Hello again, ${name}`);
addUser((name) => {
console.log(`Hello again and again, ${name}`);
});
});
});However, callbacks are often used to continue code execution after an asynchronous operation has completed - these are called asynchronous callbacks.
Its is often to named callbacks function done - Because we are calling them when the operation done:
function saveUser(user, done) {
// do something async
done();
}
console.log("Start saving user...");
saveUser({ name: "David" }, () => {
console.log("User saved!");
});Lets fix the above intro.js script by adding a callback function.
Create a callbacks-logging.js.
Change the logAfterMs function to accept a callback function as a the last argument:
function logAfterMs(message, ms, done) {
// implement
}- Write a code that log the string
"1"after 0 ms. - Write a code that log the string
"2"1 ms after the log"1"above has finished. - Write a code that log the string
"3"10 ms after the the log"2"above has finished. - Write a code that log the string
"4"5 ms after the the log"3"above has finished. - Write a code that log the string
"5"after all the logs.
Commit and push your changes.
In Node.js, it is considered standard practice to handle errors in asynchronous functions by returning them as the first argument to the current function's callback. If there is an error, the first parameter is passed an Error object with all the details. Otherwise, the first parameter is null.
Here is an example with synchronous code:
const exampleValue = null; // change to test the function
function validateTruthy(value, done) {
if (value) return done();
const error = new Error(`Value ${value} is not Truthy!`);
done(error);
}
validateTruthy(exampleValue, (err) => {
if (err) console.error(err);
else console.log("Great success!");
});We want to be able to handle errors in our logging code workflow.
Here is a function that simulate a logging that may fail:
function maybeLog(message, errorChance) {
const isError = Math.random() < errorChance;
if (isError) throw new Error("Something went wrong");
console.log(message);
}Create a maybeLogAfterMs function that simulate a logging that may fail.
function maybeLogAfterMs(message, ms, done) {
// implement me, you can use the maybeLog function
}Lets say the "3" logging may fail.
- Replace step 3 (logging
"3") with thismaybeLogAfterMsfunction. - Handle the error:
- If the error is thrown, log the error and stop the execution.
- If the error is not thrown, log the string
"4"as usual and continue to step 5.
Commit and push your changes.
We want our code to pass results from async operations to other.
Lets say the "3" logging need to pass the chance of the error to the "4" logging after successful logging.
- Update the
maybeLogAfterMsfunction to call the callback function with the chance of the error. Don't forget that callback first argument is the error! (How do you pass no error?)
function maybeLogAfterMs(message, ms, done) {
const errorChance = Math.random();
// implement me.
}-
Handle the results in step 4:
- If the results is bigger then
0.8log"4: This is very rare!" - If the results is smaller then
0.8log"4: This is very common!"
- If the results is bigger then
-
Print the
"5"logging afterrandom(from step 3) seconds.
Commit and push your changes.
We want to be able to run multiple async operations in parallel.
Lets say that after the "4" logging we want to print the "5" logging and in concurrent run functions that logs the string "hi 5" after 5 ms and "take 5" after 6 ms.
- Create a function that after
5ms logs the string"Hi five" - Create a function that after
6ms logs the string"Take five" - Execute the
"5","Hi 5","Take 5"functions logging after the"4"logging in concurrent.
Commit and push your changes.
We want to be able to run async operations after parallel execution is done.
Lets say that after the step 5 (logging "5", "Hi five", "Take five") we want to log "6" after 10 ms;
How do you implement a job after "Parallel" execution?
- What is the advantage and the disadvantage of using callbacks as a solution for the async problem?
- When do you think this solution will be useful?
- What is callback hell?
These topics are not covered in this chapter but is worth knowing:
A Promise is an object representing the eventual completion or failure of an asynchronous operation.
Once a promise has been called, it will start in a pending state. This means that the calling function continues executing, while the promise is pending until it resolves, giving the calling function whatever data was being requested.
The created promise will eventually end in a resolved state, or in a rejected state, calling the respective callback functions (passed to then and catch) upon finishing.
Most of the time, you will consume an a already-created promises, but it important to understand how to create a promise.
Here is a basic example using function that "generates" a promise:
const getHello = () => new Promise((resolve) => resolve("Hello World!"));
getHello().then((result) => {
console.log(result);
});Here is an example utilizing reject:
const addAsync = (x, y) => {
return new Promise((resolve, reject) => {
if (x === undefined || y === undefined) {
reject(new Error("Must provide two parameters"));
} else {
resolve(x + y);
}
});
};Aren't promises just callbacks with .then()?
Well, .then() and .catch() always return Promises. That enables us to create arbitrary long chains of method calls:
asyncFunc1()
.then((result1) => {
/*···*/
return asyncFunc2();
})
.then((result2) => {
/*···*/
return syncFunc3();
})
.then((result3) => {
/*···*/
return result;
})
.catch((err) => {
/*···*/
return "default value";
});Lets create the infamous logging example using promises.
-
Create a
promise-logging.js. -
Write a code that does the same as the code in Callbacks Execution order but using promises. Commit and push your changes.
-
Write a code that does the same as the code in Callbacks Error Handling but using promises.
- Create a
maybeLogfunction that returns a promise that simulate a logging that may fail. - Handle the error:
- If the error is thrown, log the error and stop the execution.
- If the error is not thrown, log the string
"4"as usual and continue to step 5.
- Commit and push your changes.
- Create a
-
Write a code that does the same as the code in Callbacks Pass data but using promises.
-
Replace the
maybeLogfunction with function that resolve with the random number (like the callback example). -
Handle the results in step 4:
-
If the results is bigger then
0.8log"4: This is very rare!" -
If the results is smaller then
0.8log"4: This is very common!"
-
-
Write a code that does the same as the code in Callbacks Concurrent execution but using promises. You may add the After Concurrent job.
Commit and push your changes.
- What is the advantage and the disadvantage of using promises as a solution for the async problem?
These topics are not covered in this chapter but is worth knowing:
.finally()util.promisify()
Async Await is a new way to write asynchronous code and is kind a "syntactic sugar" for promises. Moreover it is has a better DX (async stack trace support and debug step through)
An async function is a function declared with the async keyword, and the await keyword is permitted within it. The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.
Here is a basic example:
async function asyncExample(value) {
const syncRes = syncOperation(value);
const resultsFromAsync = await asyncOperation(value);
const asyncRes = await anotherAsyncOperation(resultsFromAsync);
const res = { asyncRes, syncRes };
return res;
}Lets refactor the promise-logging.js using async await:
-
Create a
async-await-logging.js. -
Change the
setTimeoutto promise based usingnode:timers/promises:import { setTimeout } from "node:timers/promises";
-
Write a code that does the same as the code in Callbacks Execution order but using async-await. Commit and push your changes.
-
Write a code that does the same as the code in Callbacks Error Handling but using async-await. Commit and push your changes.
-
Write a code that does the same as the code in Callbacks Pass data but using async-await. Commit and push your changes.
-
Write a code that does the same as the code in Callbacks Concurrent execution but using async-await. You may add the After Concurrent job.
Commit and push your changes.
- What is the advantage and the disadvantage of using async await as a solution for the async problem?
- Should we use await inside
fororwhileloop? - Should we use await inside a
forEachloop?
These topics are not covered in this chapter but is worth knowing:
- Top level await
- Async Iterators (
for await) -
return awaitin async function
The Promise class has static combinator functions that help us work with arrays of promises.
Promise.all() is a function Which get array of promises and returns a single Promise which is:
- Fulfilled with the array of the fulfillment values of the input promises.
- Rejected if at least one Promise is rejected. The value is the rejection value of that Promise.
Something like this:
[...promises] => Promise([...res])More accurate in typescript syntax (Advanced):
Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>Promise.all is important because it let us execute concurrent jobs on arrays using asynchronous .map().
Here is an abstract Example:
Promise.all(arr.map(async (element) => await asyncLogic(element)));Let's create an practical example!
downloadText() uses the Promise-based fetch API to download a text file into a local directory called downloads.
import { writeFile } from "node:fs/promises";
import { join, basename } from "node:path";
const downloadText = async (url) => {
const res = await fetch(url);
if (!res.ok)
throw new Error(`Failed to download "${url}" - ${res.statusText}`);
const text = await res.text();
const filePath = join("downloads", basename(url));
await writeFile(filePath, text);
};- Create a function called
downloadTextFileswhich get an array of urls and download all the files into thedownloadsdirectory. - Add log before and after each download, and after all downloads are done.
What is the execution order of the logs?
Test your function using this example:
import { access, mkdir } from "node:fs/promises";
const files = ["sample1.txt", "sample2.txt", "sample3.txt"];
const urls = files.map(
(fileName) => `https://filesamples.com/samples/document/txt/${fileName}`
);
await access("downloads").catch(() => mkdir("downloads"));
await downloadTextFiles(urls);Promise.allSettled() is a function Which get array of promises and returns a single Promise which is fulfilled when all promises are settled.
The fulfillment value is an array of the objects with the result of the promises.
Something like this:
[...promises] => Promise([
{
status: 'fulfilled' | 'rejected',
[value | reason]: res | err
},
...,
])More accurate in typescript syntax (Advanced):
Promise.allSettled<T>(promises: Iterable<Promise<T>>): Promise<Array<SettlementObject<T>>>
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}Wow, this looks pretty exhausting. Why using Promise.allSettled() instead of Promise.all()?
Unless there is an error when iterating over promises, the output Promise is never rejected. This lets us execute asynchronous functions in concurrent using map() without throwing if some "jobs" failed.
Lets fix the downloadTextFiles to use Promise.allSettled() instead of Promise.all().
- After the download is finished, print the number of failed downloads and the reason for the failure.
You can test your function using this example:
const files = [
"sample1.txt",
"not-exists1.txt",
"sample2.txt",
"not-exists2.txt",
"sample3.txt",
];should print after all downloads are done:
Failed to download 2 files:
Failed to download https://filesamples.com/samples/document/txt/not-exists1.txt - Not Found
Failed to download https://filesamples.com/samples/document/txt/not-exists2.txt - Not FoundSometimes we want to stop executes all "jobs" when an error ocurred.
- Can we use
Promise.all()? test it with the previous example. - Create a function called
downloadAllTextFileswhich get an array of urls and download all the files into thedownloadsdirectory.- Add log before and after each download, and after all downloads are done.
- Stop the execution if there is an error.
- Did you run all jobs concurrently?
These topics are not covered in this chapter but is worth knowing:
Promise.race()Promise.any()
NOTE: I recommend to advice with your mentor if you should learn this section and how much time you should spend on it.
Node.js is famous for its asynchronous and event-driven nature.
Node has a built-in event emitter that allows us to create event-driven programs using the events module.
Here is a basic example:
const EventEmitter = require("node:events");
const eventEmitter = new EventEmitter();
eventEmitter.on("start", () => {
console.log("started");
});
eventEmitter.emit("start");
// logs "started"An event handler is a particular type of callback.
Usually we prefer using promises to handle async operations but event handlers are still useful in some cases.
Lets create the callbacks-logging.js using event handlers.
-
Create a
event-handler.js. -
Write a code that does the same as the code in Callbacks Execution order but using event handlers.
Commit and push your changes.
-
Write a code that does the same as the code in Callbacks Error Handling but using event handlers.
Commit and push your changes.
-
Write a code that does the same as the code in Callbacks Pass data but using event handlers.
Commit and push your changes.
-
Write a code that does the same as the code in Callbacks Concurrent execution but using event handlers. You may add the After Concurrent job.
Commit and push your changes.
- What is the advantage and the disadvantage of using event handlers as a solution for the async problem?
- When do you think this solution will be useful?
These topics are not covered in this chapter but is worth knowing:
-
captureRejectionsand async event handlers (not recommended)
If you finish the above exercises in the "Estimation time" you can move on to the advanced topics.
this is a work in progress and will be updated soon.
-
What the heck is the event loop anyway? | Philip Roberts | JSConf EU
-
process.nextTick queue,promises microtask queue,macrotask queue
I Want YOU To Join The Army.