# Promises ## Passing data to callbacks In the previous set of notes, we used callbacks as a way to run some code *when* something happens. However, we often want to know *what* happened. In this case, we can pass the result of an asychronous action as an argument to the callback function. We'll look this week at some of the asynchronous functions that make up Node.js's filesystem API. The full set of functions can be found in the [documentation for the `fs` module](https://nodejs.org/api/fs.html). There are quite a lot, so we'll look at four of the most useful ones: - [`fs.readFile()`](https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback), used to read the entire contents of a file - [`fs.writeFile()`](https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback), used to set the entire contents of a file - [`fs.readdir()`](https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback), used to list the files in a directory - [`fs.stat()`](https://nodejs.org/api/fs.html#fs_fs_stat_path_options_callback), used to get metadata about a file (e.g. its size, when it was last modified, etc.) To illustrate the first two of these, [here](write-echo.js) is an example program that writes to a file and then reads its contents: ```js // Import Node.js's 'fs' module const fs = require('fs') // Write 'hello, world' to the file fs.writeFile('something.txt', 'hello, world', (err) => { if (err) throw err // Now read the file; it should contain 'hello, world' now. // 'utf8' indicates that the file is a text file encoded in UTF-8. fs.readFile('something.txt', 'utf8', (err, data) => { if (err) throw err // Prints 'hello, world' console.log(data) }) }) ``` ## Callback hell — nesting You may have noticed that the asynchronous programs above require significantly more code than their synchronous counterparts. For example, we would write something like this instead in a synchronous language: ```js writeFile('something.txt', 'hello, world') console.log(readFile('something.txt', 'utf8')) ``` The general observation that using callbacks tends to make programs more complicated is called "callback hell". One aspect of callback hell is that, as we saw last time, asynchronous tasks that happen *in sequence* must be nested inside the callbacks of the previous tasks. For example, [here](size-sum.js) is a program that computes the total size of 3 files: ```js fs.stat('a.txt', (err, statsA) => { if (err) throw err fs.stat('b.txt', (err, statsB) => { if (err) throw err fs.stat('c.txt', (err, statsC) => { if (err) throw err const totalSize = statsA.size + statsB.size + statsC.size console.log(`Total size: ${totalSize} bytes`) }) }) }) ``` ## Callback hell — error handling Error handling in asynchronous code is tricky. Does the following code catch the error that is thrown inside the `setTimeout()` callback? ```js try { setTimeout(() => { throw new Error('Error from timeout') }, 1000) } catch (e) { console.error(`Error was caught: ${e}`) } ``` The answer is no. Remember that `setTimeout()` returns as soon as the callback has been set up, without waiting for the 1 second to elapse. Therefore, the `try` block ends before the error is thrown. So how can we "catch" an error that occurs during an asynchronous action? As you can see in the examples in the previous sections, all of Node.js's asynchronous functions pass an `err` as the first parameter to the callback. If an error occurred, `err` is set to the `Error` object that was "thrown". Otherwise, `err` is set to `null`. For example, `fs.stat()` gives an error if the file we try to read doesn't exist. We can use this to create a file if it doesn't exist: ```js // The arguments are the file to create and its initial contents. // Run the program using node create-file.js FILENAME CONTENTS fs.stat(process.argv[2], err => { if (err) { // File didn't exist, so create it fs.writeFile(process.argv[2], process.argv[3], err => { if (err) throw err console.log(`${process.argv[2]} created`) }) } else console.log(`${process.argv[2]} already exists`) }) ``` If we run [this program](create-file.js) using `node create-file.js new-file.txt abc`, it will make the file `new-file.txt` with contents `abc`. If we run it again with `node create-file.js new-file.txt def`, it won't change the file because it already exists. But most of the time, we don't want any special error handling. If an error happens in a sequence of asynchronous actions, we usually want to stop and report an error from the whole sequence. As you can see in the examples above, writing `if (err) throw err` for every easynchronous action gets quite tedious. ## What is a `Promise`? In JavaScript, a `Promise` represents an asynchronous task that "promises" to compute a value eventually. We write `Promise<T>` to denote a `Promise` that will compute a value of type `T`. This may sound very abstract, so here are some concrete examples: - Reading a file's contents can be represented as a `Promise<string>` - Looking up a user in a database can be represented as a `Promise<User>` - An asynchronous task that *doesn't* compute a value can be represented as a `Promise<undefined>` It is important to keep in mind that a `Promise<T>` is different from a `T`. **There is no way to "get" the `T` from a `Promise<T>`**. When a `Promise` computes a value, this is called "resolving" to the value. All `Promise`s can also "reject" with an error. ## `Promise` methods ### `.then()` This method is used to run `Promise`s in sequence. Because running asynchronous tasks in sequence is extremely common, `.then()` is the primary way to compose `Promise`s. [Here](copy-promises.js) is how it is used: ```js /* Note the `.promises`. `require('fs').promises` has the same functions as `require('fs')`, but they return Promises instead of taking callbacks. */ const fs = require('fs').promises // Read 'a.txt'. `readPromise` is a `Promise<string>`. const readPromise = fs.readFile('a.txt', 'utf8') /* After `readPromise` finishes, write the result to 'b.txt'. `contents` will be the `string` that was read. We return a `Promise<undefined>` that represents the write. */ const writePromise = readPromise .then(contents => fs.writeFile('b.txt', contents)) // Once the write finishes, print a message writePromise.then(_ => { console.log('Copying done') }) ``` In general, if we have a `Promise<A>` and a function `A => Promise<B>`, we can use `.then()` to chain them together into a single `Promise<B>`. You can call `.then()` multiples times on the same `Promise` if multiple actions need to happen after the `Promise` finishes. For [example](copy-twice.js), to copy `a.txt` to both `b.txt` and `c.txt`: ```js const aPromise = fs.readFile('a.txt', 'utf8') aPromise.then(contents => fs.writeFile('b.txt', contents)) aPromise.then(contents => fs.writeFile('c.txt', contents)) ``` ### `Promise.resolve()` This function is used to make a `Promise` that immediately resolves to a given value. This is useful for a function which sometimes performs an asynchronous action but sometimes returns a pre-computed value. For [example](cache.js), we can save the contents of a file after reading it the first time: ```js let cachedContents function getContents() { // If we have already read the file, just return its contents if (cachedContents !== undefined) { return Promise.resolve(cachedContents) } // Otherwise, read the file and store its contents const readPromise = fs.readFile('file.txt', 'utf8') readPromise.then(contents => { cachedContents = contents }) return readPromise } ``` ### `.catch()` Error handling with `Promise`s is much easier than with callbacks since `.then()` automatically handles errors that occur. In [this example](catch-file.js), we concatenate the contents of two files. If `a.txt` doesn't exist, then `fs.readFile()` will reject and the `aContents => ...` function will never be called. If `a.txt` doesn't end with a newline, we throw an error, which will also cause the `Promise` to reject. The `err => ...` function will be called if the `Promise` rejects for any reason. It reurns a new `Promise` to run instead. Note that the `concatenated => ...` function will always be called because we catch any error that could have occurred. ```js fs.readFile('a.txt', 'utf8') .then(aContents => { // If the contents of a.txt don't end with a newline character, // reject with an error if (!aContents.endsWith('\n')) { throw new Error('Missing newline at end of a.txt') } // Otherwise, read b.txt and concatenate the files' contents return fs.readFile('b.txt', 'utf8') .then(bContents => Promise.resolve(aContents + bContents)) }) // If either file didn't exist or a.txt didn't end with a newline, // resolve to the empty string instead .catch(err => Promise.resolve('')) // Finally, write the string to c.txt .then(concatenated => fs.writeFile('c.txt', concatenated)) ``` ### `Promise.all()` `Promise.all()` takes an array of `Promise`s and returns a new `Promise` that resolves once *all* of the `Promise`s have resolved. When it resolves, it computes an array containing the result of each of the `Promise`s. In [this example](recursive-mtime.js), we find the most recent modification time of all files inside all subdirectories of a given directory. (For example, if `a/b/c.txt` was modified, that counts as a modification of `a/b` and `a` as well.) ```js // Returns a Promise<number> that computes the most recent // modification time for any file inside a directory const modifiedTime = path => fs.stat(path).then(stats => { // mtimeMs is the modification time for this file/directory const {mtimeMs} = stats if (stats.isDirectory()) { // If this is a directory, compute the modification time // for all files/subdirectories inside it return fs.readdir(path) .then(files => // Compute modification times of all contained files Promise.all(files.map(file => modifiedTime(path + '/' + file) )) ) // modifiedTimes will be an array containing the latest // modification time of each file in the directory .then(modifiedTimes => Promise.resolve(Math.max(mtimeMs, ...modifiedTimes)) ) } // If this is a file, just return its modification time else return Promise.resolve(mtimeMs) }) ``` ## *Aside*: monads If you've ever used Haskell, you might have noticed that `Promise`s look a lot like its `IO` monad. `.then()` is the equivalent of `>>=` ("bind"), which chains two IO operations together. And `Promise.resolve()` is the equivalent of `return`, which wraps a normal value in an instance of `IO`. Just like monads in Haskell allow you to write code that looks imperative in a functional language, `Promise`s let you write asynchronous programs that look like blocking programs, while still using callbacks under the hood. Towards the end of the course, we'll also cover JavaScript's `async`-`await` notation, which hides even the calls to `.then()` and `Promise.resolve()`. This is very similar to `do` notation in Haskell.