Promises & Generators in Node.js

Ben August speaking at London Node User Group in February, 2017
1127Views
 
Great talks, fired to your inbox 👌
No junk, no spam, just great talks. Unsubscribe any time.

About this talk

As part of ES6, node finally has native support for promises and generators. In this talk the speaker will introduce you to the concept and syntax of both, show you some ways you may use them in the real world and give a quick lookout what's up next.


Transcript


All right. Hi, guys. I want to talk to you about promises and generators in Node. I actually wanted to talk to you about ES6 and the features in general, but just the generators alone can probably take up to 30 minutes. I thought we'd just talk about that for now. Get a little into that. Promises and generators together can be really powerful. And I want to show you a few things you can do with it. And one pattern in particular that I really enjoy using. I must say, I've mostly built APIs with Node for something or another. And the first time I heard about callback was probably after using Node for a year or two. It was never really an issue to me, and I started kind of complaining. Like, "Hello World" is too complicated in JavaScript. It can be if you chose to. That actually works as well as JavaScript. But there was another issue that I had with callbacks. You all probably saw this piece of code. You've probably all written it. Some of you maybe have a keyboard shortcut for it. And that got quite annoying. And promises really could help us out as well. And, finally, we have native promises in Node, which is awesome. And so basic syntax looks like this pretty much. So you're probably familiar with it if you have used any kind of library. The difference you might see is a 'catch.' Usually, you might use a 'fail' or whatever. But that's the basic syntax. And I thought a user would find this as an example because it's something everybody uses at some point, and it sends a core API. We don't need any library for it. It just works. There is just one small problem, and that is it doesn't work. Because Node core API doesn't support promises. It doesn't return promises. While we do have promises natively in Node, all the core API codes return as Callbacks. They don't return a promise. And there are more or less good reasons for it. You can find the discussion on GitHub if you're interested. The two main reasons that I've got... All of it are pretty much backwards compatibility. So, you don't want to break stuff just because you remove the callback and it starts returning promising. And the second bit is we want Node Core to stay lean and not get too bloated. So having both in there would be unnecessary really. So those are pretty much the main reasons. And whether they change it at some point or not is open, but it doesn't look like it. So just something to be aware of. It's not a big deal or anything. Yeah, just be aware of that. Let's go through how it actually looks like. It's pretty straight forward. You just have a promise class, a promise keyword. You use a new keyword like you would with any other class to instantiate a new promise. And, funnily enough, you give this promise a function so, essentially, a promise works on a callback, technically. So it just builds on top of this technology. So you get this new promise message, and this message takes two parameters. Which are, by convention, resolve and reject. Which, on its own, are essentially just callbacks. And you do whatever you want to do, and then call either resolve or reject depending on what you want to do. Realize it was successful, what you did, or there was an error. And then if you want to use your promise you created, you just go on like we showed before with the name 'catch.' So if you go back to our example of a writ file, we could do something like so. Just create a new promise, do our read file thing, and the callback, depending on whether we have an error or not, we call resolve or reject. So, pretty straight forward. But you probably don't want to actually do that. Promise [inaudible] are awesome, but if you start doing it yourself like this, you just add more work than you get value out of it, in my opinion. But, again, the libraries that can promise-fy everything for you if you really wanted to. But the general idea how you can use promises is pretty much like so. And then you can do whatever you want with it. So what else we can do with promises, pretty much the same as with all the libraries as well. So the most obvious is you can change them. So, when you return the promise, and you are [inaudible], you can just go on and on and on. And that is the main thing how you get out of the so-called callback hell. You can just chain promises on and on and build upon them. This example is a little silly, but if you had a file, in this file there would be some text or parts to another file. We could take this, it would return, and then could read that file as well, and build upon the result we got before. The chaining thing is probably the main thing you want to do with promises, really. The other thing is 'Promise.all'. You have probably the same stuff in all the other libraries as well. And basically what it does is you give it an array of promises, and it runs all promises simultaneously. And once they are all done, you go into your .then and can do stuff with the results. Or, if any one of those promises failed, you go straight to catch and ignore all the other results. So if you do 'Promise.all,' just be aware that as soon as just one of those fails you completely ignore all the results and go straight to the catch. Usually you want that because you need all to work out. Just need to be aware of that. And then we have 'race' which is pretty much exactly what it sounds like. You, again, give it an array of promises, and whichever promise returns something first gets resolved or rejected. You can use it, for example, to run the same kind of promise against two different systems to just see which one is faster, and use that. Just be aware, the first one that rejects, again, goes straights to catch, and you ignore the other one. So if you had one that fails before the other one resolves, you still go straight to catch and ignore the other one. So be aware of that. If you have something that fails very quickly, you might not want to use that. Or make sure that you really want to fail, in that case. So that's pretty much all the kind of stuff you can with native promises. So it works very similar to what you have used before, most likely. It's nothing too crazy. But native field, that's great. So, a generator. Generators are really fun stuff. Weird stuff. It'd probably take some time to get really around that concept, what it means. And, basically, a generator is just a function that you can pause and then jump back into at another time. And that probably doesn't make too much sense to you. Let me just go through some examples and see what it actually means. The basic syntax is just like so. The two things you will notice is the funny star. If you put a star in front of the message name, it's a generator. And that's just the syntax. If you come from any other language, you probably know the star for stuff like pointers and so on. Obviously, we use it for something entirely different. It has nothing to do with that. So if you know it from different language, ignore it. Via JavaScript, we do a lot of stuff how we want it. The other keyword is 'yield. ' Yield is, essentially, a keyword where you then pause and jump back into it. How that actually looks like in practice, I'll show you later on, again. Just keep it in mind. Yield is essentially the keyword you need to make use of a generator. And that's pretty much all there is to it. Now, how we would actually use a generator would be like so. So you create an instance of your generator. Keep in mind, without the new keyword because, again, consistency. And just create a new instance. So you can, for example, create more instances of the same generator as well, if you wanted to. And then you run the message 'next' on the generator. That's just like it is. And the return you get from it is an object, actually. An object with two properties. One being the value, the thing that you actually return, and the other one being done, which is a buoy true or false. And if we were to run it on our example above here, you will notice that it actually returns as 'done: false.' You would probably expect it to be true. However, how it works is, basically, the first time you call 'next' you jump in at the very top at 'function' and go to the first 'yield.' And afterwards, you continue at the yield and go to the end of the function. Even though there is nothing behind it, your code doesn't know it. So it jumps back into it. It goes through. If we were to call 'next' again, it would give us 'value: 'undefined'' and 'done: true'. So it's a very easy way around it. You can just use return instead of yield. If you use return, it returns you as a value, as usually. But the done is automatically to 'true.' All right. So that's the very, very basic syntax of it, and how it looks like. So let's look at a better example, maybe. You use generators often for iterators. It's a pretty common use case. This example, again, is... You probably wouldn't do it exactly like this in practice, but just as an example, you could run a value loop forever and just yield the result every time you go through it. And then, if you call next, it just gives you back a zero. If you call next again, it gives you back the one. And so on and so on. But the important difference is that you can pause it. You can do whatever else you want in between. Which gives you a lot of power to do stuff in between. So, for example, what things you could do, maybe if you were to make a game. A classical example would be a game loop. You can jump out and do random other stuff. Or a lazy loading in some kind or another. Those are the kind of things you could do with it. But important to note here is that it's still Node. We are still single-threaded. It's not as you could sync [inaudible] anything like that. If you are aware of the event loop and how that all works, you just put it like a callback or a promise on top of this stack and go from there. So just keep that in mind. We are still Node, still single-threaded, and you still have to think about it just as much as with callbacks or promises. So you can do quite a lot of cool stuff with it, but I want to show you some more stuff that I personally use it most for in my work with APIs. Which I think is pretty cool and gives you a lot of power to do stuff. So imagine if you go back to the 'readFile' thing. We could do something like so. Instead of using a callback or promise, we just yield it and then can start working with it right away. That, obviously, doesn't work like this because you can't use 'yield' outside of a generator, first of all. And, secondly, somehow you need to know when 'readFile' is done and jump back into the generator and continue using it. So let's look, first, how that could look like. So if you had an API, and we have our endpoint, and we have our normal request handler, the request handler itself could be encapsulated in a generator. You'll notice a little star. It's an anonymous function, so it's just right in front of the brackets. It's indicating generator. And now we can 'yield' the 'readFile' and return the file content, and either do something with it or just return it straight to the user if you had a static website or whatever. Now, you'll notice that I added this little message called 'run' here. I called it 'run.' And we'll go through what it does on the next slide. However, you can essentially ignore that entirely. That is a bit that libraries can take over for you. So let's look at what this 'run' does because there is still something important we are missing. So, as I said, you can completely ignore this. Obviously, way too to go through now. And I'll show you later some libraries that do exactly that for you. But still, I thought it's important to at least explain the concept, so you know what's happening behind the scenes. And the first thing you'll notice is we just return a promise. So this whole pattern is based on promises, just as promises are based on callbacks. So, essentially, what happens is on this 'run' message we return a promise, and we get an instance of the generator, and then just step through it. And by step I mean you pause it and you unpause it. So for every time you 'yield' it goes into the step, looks into the return object, checks whether 'done' is 'true.' If that's the case, then we just resolve the promise, and we are done with the generator. The generator finished. If that's not the case, we wait until the promise is resolved. So important to note here in this example, you have to 'yield' a promise. Otherwise, it doesn't work. So it 'yields' a promise. And wait until the promise is resolved. As soon as it's resolved, we step back into the generator and can continue with our method. What that means is, we can just use it in a really, really cool way. All its logic is now in here. All the error handling is in here. And that allows us to write very lean and clean code. So if we have, for example, again, an API, and we have a request handler that is supposed to create a new user, it could look like so. We could, for example, ask for the check if a user with that email already exists, do our other promise request, and just 'yield' for it. And we don't have to do any kind of callbacks or promises or anything. We just use the keyword 'yield,' and then we can start using the user right away as if it were a synchronous code. And we don't even have to worry about the arrows at this point in time because the promise before and afterwards do that for you. So at the point here, it's really just about your logic. You look at your logic, and all the other stuff that is distracting you from the logic, the business logic, is elsewhere. So we are asking here for checking here if the user already has an email. If that's the case, we can just use return like we always do in every other function. And everything else in this generator doesn't get executed anymore. And that's because if you use return, the 'done' property will be 'true,' and will resolve the promise right away. So you can use it just like any other method. You don't have to wrap your head around strange concepts. It works just the same. Just that it allows you to use 'yield' as well. So, for example, the next step could be we want to write the user to the database. We, again, just use 'yield.' We don't care about anything else. And we can use the new user straight away because, technically, we just jump back into the generator once it's done. But for our code, how we write it, what it looks like, it's synchronous, kind of. Which is really powerful and gives you a lot of options. So, in this example, we could wait now, until the user is written to the database and then send out an email through a third-party provider, do it ourselves or whatever, and then just return our account as created. And I personally think that it's a great thing to do. If you're aware about promises, how much better they are compared to callbacks, I'm sure you'll love that because it's just even so much better. And, important to note, since our 'run' message returns a promise, we can just go on like this. So our 'getUserByEmail' message could be a generator as well and could make use of the 'yield' function as well. So you can use it throughout your code base just like any other method. And you don't have to worry about anything. So, you probably noted, it is the same thing as async/await. Async/await doesn't currently exist in Node, but it is in the proposed ES7 spec. So what we do now is using the cool, awesome ES6 features to simulate features that are proposed for ES7. Just as we did in ES5 with the ES6 features. But we needed all the features to make it happen. Without promises, the whole pattern doesn't work. Without callbacks, promises wouldn't work. So it makes sense to go step-by-step through it, like so. But, in a year or two, hopefully, we have some natively supported as well with async/await. Just very exact same pattern. And it would probably look like this. The proposal, at least, says it should look like so. And you'll notice we have the 'async' which basically replaces the star to say it's a generator. And we have 'await,' which replaces the keyword 'yield.' Apart from that, it works the exact same way. Until then, there are, obviously, a lot of libraries that can help you do that, such as Babel, CO [SP], or however you pronounce it, and other libraries by Facebook or Google or whoever. So if you use, already, some kind of modernizer, then potentially you can use that pattern right away. And then you don't need to worry about this 'run' method that does all this magic stuff because they do it for you. And they have all a little different syntax, so we have to look at the docs. How exactly they do it. But the general idea is the same. And that is, in my opinion, one of the coolest things you can do with those new features with the new generators. And I personally love it. I hope you do too. And, yeah, thanks! Yes? - It's about the asynchronous aspect. So, it's just another way to deal with what we would have used callbacks for beforehand. So everything that doesn't return a value right away, that needs some time, like a database call, we need to wait until it's done without blocking the process. So we use things like callbacks or promises. Yeah, and that is just a pattern to deal with that in a nice way. - No, it's also asynchronous. So all these things here, the 'getUserByEmail' is essentially a promise. So, if you have this example here, this 'run' message returns a promise. So this whole pattern's basically just a way to kind of replace a callback promise pattern to make it more nicely really. If you're not aware of all the ES6 features, I use them. So, I'm not sure if you're aware of the arrow method and how they work. But, essentially, they are a function. And if you don't use [inaudible], it's like return it automatically. If you were to use [inaudible] like... I don't know if I have it somewhere... But then you need to use the return. But, yeah, the arrows are just a nice new shorthand for that. - Okay. Thanks so much. - Thanks.