# `async`-`await` ## Recap of `Promise`s JavaScript's `Promise` abstraction was discussed in the [notes](../promises/promises.md) near the start of the course and you had a chance to use it on the Make project. I'l give a quick refresher since `Promise`s are essential to the `async`-`await` syntax. A `Promise` represents an asynchronous task or computation. When the task or computation finishes, the `Promise` "resolves" to a value. If an error occurs, the `Promise` "rejects" with the error. The usefulness of `Promises` comes from the ways they can be composed into more complex `Promise`s. There are two main ways to combine `Promise`s: - In sequence. `.then()` chains two `Promise`s into a `Promise` that performs the asynchronous tasks or computations one after another. You can build long chains by calling `.then()` more times: ```js // Build a Promise that performs 4 asynchronous computations in sequence promiseA .then(a => { /* build and return promiseB... */ }) .then(b => { /* build and return promiseC... */ }) .then(c => { /* build and return promiseD... */ }) ``` - In parallel. `Promise.all()` combines an array of `Promise`s into a `Promise` that performs all the tasks or computations in parallel and resolves to an array containing all their results: ```js // Build a promise that performs 3 asynchronous computations in parallel // and resolves with their results once they have all finished Promise.all([promiseA, promiseB, promiseC]) ``` ## `async`-`await` syntax `Promise`s can greatly simplify asynchronous programs. Frequently, several asynchronous tasks need to happen in sequence. In this case, `.then()` provides an interface that looks closer to how the program would be written in a synchronous language. Recently, new syntax was added to JavaScript that makes asynchronous code look even more like synchronous code. It relies on two keywords: - `async`: we can declare an "async function" by putting `async` in front of it: ```js async function name(...arguments) { /* ... */ } // or async (...arguments) => { /* ... */ } ``` Unlike normal functions, `async` functions return `Promise`s. If the function `return`s a value, the `Promise` resolves with that value. If the function `throw`s an error, the `Promise` rejects with that error. - `await`: inside an `async` function, the keyword `await` can be used to "wait" for a `Promise` to resolve. If the `Promise` throws an error, it can be caught by a `try`-`catch` block. It is easiest to explain `async`-`await` with some examples. ## Example: recursively exploring a directory We can rewrite the code from the `Promise`s notes for [computing the modification time of a directory](../promises/promises.md#promise.all). Recall that we want to find the latest modification time of any file inside the directory (or any subdirectories). By making `modifiedTime()` an `async` function, we can use `await` instead of explicit `.then()` calls on `Promise`s. You can compare this [`async`-`await` version](await-mtime.js) with the [`Promise`s version](../promises/recursive-mtime.js). ```js // Returns a Promise that computes the most recent // modification time for any file inside a directory async function modifiedTime(path) { // First, we wait for the fs.stat() Promise const stats = await fs.stat(path) // Use the modification time of the file or directory itself let latestTime = stats.mtimeMs // If this is a directory, check all files inside for later modified times if (stats.isDirectory()) { // Wait to get a list of all files/subdirectories in the directory const files = await fs.readdir(path) // Then get the modified time of each file/subdirectory for (const file of files) { const fileModifiedTime = await modifiedTime(path + '/' + file) // update latestTime if the modification time is later latestTime = Math.max(latestTime, fileModifiedTime) } } // Return the latest modification time return latestTime } ``` However, this code no longer explores subdirectories in parallel, which was the main advantage of asynchronicity. Can you see why? The issue is that we have replaced `Promise.all(files.map(file => /* ... */))` with a `for (const file of files)` loop. Since `await modifiedTime(path + '/' + file)` *waits* for `modifiedTime()` to finish, the loop waits for `file` to be completely explored before continuing. This means the subdirectories are explored sequentially. If we want to perform asynchronous tasks in parallel inside an `async` function, **we still need to use `Promise.all()`**. [Here](await-mtime-parallel.js) is a parallel version using `Promise.all()`: ```js async function modifiedTime(path) { const stats = await fs.stat(path) const {mtimeMs} = stats // If this path is a file, return its modification time if (!stats.isDirectory()) return mtimeMs // If this is a directory, check all files inside for later modified times const files = await fs.readdir(path) // Get the modified time of each file/subdirectory in parallel const modifiedTimes = await Promise.all(files.map(file => modifiedTime(path + '/' + file) )) // Return the latest modification time return Math.max(mtimeMs, ...modifiedTimes) } ``` ## Example: emoji search The [Unicode Consortium](https://home.unicode.org) defines the mapping from numbers to characters (including emoji) that almost everyone uses. For example, Unicode defines 65 to mean `A`, 233 to mean `é`, and 128027 to mean `🐛`. We use this mapping to make an [emoji search](await-emoji.js) that finds a Unicode character by name (e.g. `pile of poo`) and prints out the character. We first fetch the current Unicode version (updated each year) from Unicode's [`ReadMe.txt`](https://unicode.org/Public/zipped/latest/ReadMe.txt) file. We then fetch the current [`UnicodeData.txt`](https://www.unicode.org/Public/13.0.0/ucd/UnicodeData.txt) file which lists the tens of thousands of Unicode characters. Note how `await` allows us to execute 4 `Promise`s sequentially in `getUnicodeNumber()` without needing to combine them with `.then()`. ```js async function getUnicodeNumber(searchName) { // Fetch the readme to determine the latest Unicode version (e.g. 13.0.0) const readmeResponse = await fetch(README_URL) const readme = await readmeResponse.text() // read readme as a string // The last line of the readme contains the current Unicode URL, // e.g. https://www.unicode.org/Public/13.0.0/ucd/ const [latestURL] = readme.trim().split('\n').slice(-1) // Then fetch the list of Unicode characters const charactersResponse = await fetch(latestURL + UNICODE_FILE) const charactersData = await charactersResponse.text() // Each line of the data file corresponds to one Unicode character for (const characterData of charactersData.split('\n')) { // Each line has several fields separated by `;`. // The first is the character's number and the second is its name. const [characterNumber, characterName] = characterData.split(';') if (characterName.toLowerCase() === searchName) { // If this is the requested character, read its number in base-16 return parseInt(characterNumber, 16) } } return undefined } async function printUnicode(searchName) { // getUnicodeNumber() returns a Promise, so we need to await it const number = await getUnicodeNumber(searchName) // Convert the Unicode number to a character if (number !== undefined) console.log(String.fromCodePoint(number)) } ``` (`fetch()` turns an HTTP/HTTPS request into a `Promise`, as explained in the [HTTP notes](../http/http.md#aside-fetch).) ## Example: create files without overwriting existing files When an `await`ed `Promise` rejects with an error, you can catch it using a `try`-`catch` block, just like a normal JavaScript error. For [example](await-no-overwrite.js), suppose we want to create some files but not overwrite any existing files with the same name. We can use the `wx` [flag](https://nodejs.org/api/fs.html#fs_file_system_flags) to make [`fs.writeFile()`](https://nodejs.org/api/fs.html#fs_fspromises_writefile_file_data_options) reject if a file already exists. We want either all or none of the files to be created, so if one already exists, we stop and remove all the files that were already created. ```js // Creates an array of files. If any file already exists, // no files are created and an error is thrown. async function writeFiles(files) { for (let i = 0; i < files.length; i++) { const file = files[i] try { // Write to the file, but throw an error if the file exists await fs.writeFile(file.name, file.contents, {flag: 'wx'}) } catch (e) { // File already exists, so remove all the previously created files await Promise.all(files.slice(0, i).map(file => fs.unlink(file.name) )) // Re-throw the error throw e } } } // Try to create 3 files writeFiles([ {name: 'a.txt', contents: 'abc'}, {name: 'b.txt', contents: '123'}, {name: 'c.txt', contents: 'xyz'} ]) .catch(_ => console.log('Some files already existed')) ``` You may notice that the files are not written in parallel. It is possible to fix this by `Promise.all()`ing the `fs.writeFile()`s, but this is tricky since the `Promise` returned by `Promise.all()` rejects as soon as *any* of the `Promise`s reject. If the the `Promise.all()` rejected, we would need to wait for each file to finish being written before trying to remove it.