Sometimes, you might want to convert a JavaScript function that accepts a callback to one that returns a Promise
object. This lesson shows how to manually wrap a promise-based API around the fs.readFile()
function. It also explains how to use the util.promisify()
method that is built into the Node.js standard library.
Instructor: [00:00] In this lesson, I want to take a look at how we can convert a callback-based function the one that uses promises. Node heavily uses callback in its APIs. For example, if we import the FS module, we can see that when we use the read file method, we have to specify a file name and encoding, and we specify a callback.
[00:24] This callback takes two parameters. First the error, then the contents of the file just we just read. Now, this might seem a little backwards, but in node, it is convention that the error always goes first, and the body of the callback, we can node check if we had an error, and if we did we can log the error to the console using console.error. Otherwise, we can log the contents of the console.
[00:49] We're using the special file name variable here. If we run this program, we should see this file printed to the console, and sure enough here is our file. Let's now try the error case as well. Instead of reading this file, we're going to attempt to read a file that doesn't exist. This time, we see the error printed to the console.
[01:16] It would be helpful if the fs.readFile method actually return the promise. In that case, we could use the then, catch, and finally methods to attach our full_filament and rejection handlers. Why don't we just create our own version of read file that returns a promise? Here is what that could look like.
[01:36] Just like fs.readFile, our read file function should accept the file path that we want to read and encoding. The difference is that our function does not accept a callback as a parameter. Instead, we want to return a new promise.
[01:51] We're going to pass it an executive function that takes the resolve and reject functions as parameters. Within the body of the executive function, we're going to call the native fs.readFile method. We're going to pass along path and encoding.
[02:04] Now, our callback is going to look very similar to the one below. We're going to accept error and contents as parameters. When we get an error, we want to reject the promise with its error. Otherwise, we want to resolve the promise with the contents of the file.
[02:22] Let's now put our version to the test. Instead of calling fs.readFile, we're going to call our own version. We're not going to specify a callback. Instead, we're going to use .then to attach a full_filament handler and a rejection handler.
[02:43] Note that when I run this piece of code, we should see an error, because we're still trying to read the file it doesn't exist. Indeed, we see an error. Let's now try this success case. Yup, that is looking good.
[03:03] What happens though if we pass null to our read file function? In this case, the fs.readFile function will throw an error. Now, any error thrown within the body of the executive function will result in this promise being rejected.
[03:17] Everything works out just fine. If we run this again, we should see the error printed to the console. It looks a bit different now, because fs.readFile throws a type error, but we still trigger the rejection handler which is what we want.
[03:32] Everything works out just fine. Notice that a lot of the code we wrote is just boilerplate. We're creating a function that accepts the same arguments as the function that we're wrapping except for the callback. We then create a new promise, and within the executive function of that promise, we call the original function.
[03:51] We then create a new callback and we either resolve our promise or we reject it. The structure of our custom function is exactly the same for every callback-based API that we want to wrap.
[04:04] It feels like we could abstract a way this boilerplate, and indeed we can. We can import nodes util module. Once we've done that, we can create our promisified read file method by calling util.promisify, and we pass it to the callback based function as an argument.
[04:25] Now, let's give this a shot and we're going to pass filename here. Run the program. We see that it still works. In a node environment, I would recommend to use the native util.promisify method.
[04:41] If you don't have access to the util module, because you're working in another environment such as the browser, you can either go with a manual approach that we've seen before, or you can find yourself a package that implements a generic promisify method.
[04:53] As we've seen, converting callback style functions to promise based ones is mostly boilerplate. You don't want to be repeating yourself over and over again.
Hey Martin, I found this for you. I think this stack overflow should help you find the answer you're looking for.
I understand now how how to convert the callback-based function. But why should I do it in the first place? Only to use .then() and .catch(). What’s the benefit to do it?