DEV Community

Cover image for Asynchronous Javascript - 04 - Promises
Kabir Nazir
Kabir Nazir

Posted on • Edited on

Asynchronous Javascript - 04 - Promises

In this article, we’re going to be looking at an interesting feature of Javascript that was introduced in ES6 in order to run asynchronous code efficiently. Prior to ES6, for running asynchronous code (for e.g. a network request), we used callback functions. But that approach had a lot of drawbacks (including callback hell) which gave rise to issues in code-readability, error handling and debugging. In order to overcome these issues, a new Javascript object called Promise was introduced.

Promise

A Promise is a special type of Javascript object which acts as a placeholder for the eventual completion or failure of an asynchronous operation. It allows you to attach 'handlers' to it, which process the success value or failure reason when they arrive at a later stage. This lets us call asynchronous functions as if they were synchronous and store them in a proxy object, which 'promises' to return the output at a later stage of time. Let us try to understand this better with an example.

The basic syntax of a Promise is given above. A Promise is created with a function that is passed into it, called the executor function. The executor function contains the asynchronous code you wish to run. The function contains two parameters, resolve and reject. These are default callback functions provided by Javascript. The executor function is run as soon as a promise is created. Whenever the code of this function has completed running, we need to call either of the callback functions:

  • resolve(value): Calling this function indicates a success condition, with ‘value’ being the value returned from the successful completion of the executor function

  • reject(error): Calling this function indicates a failure or error condition, with the ‘error’ value being an Error object indicating the error details. ‘error’ doesn’t necessarily have to be an Error object but it is highly recommended.

The promise object returned by the constructor also has a few internal properties:

  • state: Set to “pending” initially. Changes to either “fulfilled” if resolve is called or “rejected” if reject is called.

  • result: Set to undefined initially. Changes to ‘value’ if resolve(value) is called, or ‘error’ if reject(error) is called.

Let us see how the above features work with a simple example.

Alt Text

The above code creates a promise to generate a random number from 1 to 10 and check if it’s even. We have used setTimeout in order to implement a delay of 1 second. When the promise object is created, its internal properties are set to their default values.

    state: "pending"
    result: undefined
Enter fullscreen mode Exit fullscreen mode

Let us assume that the randomNumber generated at line 2 is an even number like 4. In this case, the code at line 5 gets executed and the resolve callback function is called with the value of 4 as its argument. This moves the promise object to a “fulfilled” state. This is analogous to saying that the executor function’s task has returned a ‘success’ result. The promise object’s properties now are

    state: "fulfilled"
    result: 4
Enter fullscreen mode Exit fullscreen mode

If the randomNumber generated had been an odd number like 7, then the code at line 7 gets executed and the reject callback function is called with the Error object as its argument. This moves the promise object to a “rejected” state. The promise object’s properties now are

    state: "rejected"
    result: Error("Not an even number");
Enter fullscreen mode Exit fullscreen mode

Note that in a promise, the executor function can call only either resolve or reject once. Any subsequent calls to either resolve or reject after the first one are ignored. This is because a promise is supposed to have a single result of either success or failure. Moreover, both resolve and reject accept only a single (or zero) argument. Additional arguments are ignored.

An important thing to note is that when a promise object is created, it doesn't immediately store the output of the asynchronous operation. The output (which might be either the success value passed by the resolve function, or the error value passed by the reject function) is obtained only at a later time. This output is stored in 'result', which is an internal property of a Promise and cannot be accessed directly. In order to obtain the result, we attach special handler functions to the promise, which we shall discuss below.

then, catch and finally

Promises have three important functions, or ‘handlers’ that can be attached to them, that allow us to receive or ‘consume’ their outputs. The first one is the then handler. The basic syntax of then is as follows.

The then handler takes up to two callback functions as arguments. The first callback is executed if resolve was called in the executor function. The second callback is executed if reject was called in the executor function. For example, in the following promise, the resolve function was called in the executor function.

Hence, only the first callback was executed and the second one was ignored.

In the case of reject function being called,

The first callback was ignored and the second callback function was executed.

We can also have separate handlers to consume the results of resolve and reject. This is where the catch handler comes into play. It takes only a single callback function as an argument and executes it if the promise was rejected.

The third handler available is finally. This works similar to how final works in the normal try-catch scenario. The finally handler takes no arguments and is always executed if it is attached to a promise, irrespective of whether the promise was resolved or rejected.

We had mentioned earlier in this article about how one of the reasons promises was introduced was to overcome callback hell. The feature of promises that achieves this is the ability of chaining. The handlers of a promise, namely the then, catch and finally, all return back a promise. Hence, we can use these handlers in order to ‘chain’ multiple promises. Let’s look at a simple example.

In the above example, we have created a simple promise that resolves with a value of 10. Next, we consume this result with our first then function at line 5. This function prints the value ‘10’ into the console and then returns the value 10 * 2 = 20. Due to this, the promise returned by this then function gets resolved with a value of 20. Hence, in line 9, when the then function is being called, its result is 20. That result of 20 gets printed onto the console, followed by a return of 20 + 5 = 25. Again, the promise returned by the current then function is hence resolved with the value of 25. By repeating this, we can chain any number of promises to an existing promise. For more information regarding chaining, you can look up this document on MDN.

Now that we have looked at promises, you might be wondering where they fit into the execution order. Do promises’ handlers (then, catch and finally) go into the callback queue since they are asynchronous? The answer is no.

They actually get added to something called the microtask queue. This queue was added in ES6 specifically for the handling of Promises (and a few other types of asynchronous functions, like await). So, whenever a promise is ready (i.e. it’s executor function has completed running), then all the then, catch and finally handlers of the promise are added to the microtask queue.

The functions in the microtask queue are also given higher preference than the callback queue. This means that whenever the event loop is triggered, once the program has reached the last line, the event loop first checks if the microtask queue is empty or not. If it’s not empty, then it adds all the functions from the microtask queue into the call stack first before moving on to check the callback queue.

For more information on Promises, you could look up this document on MDN.

This concludes my series on Asynchronous Javascript. Feel free to leave a comment for any queries or suggestions!

This post was originally published here on Medium.

Top comments (8)

Collapse
 
ilya_sher_prog profile image
Ilya Sher

The function contains two arguments, resolve and reject.

Functions don't contain arguments. This is semantically incorrect. Functions have parameters.

output of the asynchronous operation.

"result" would be way better than "output" to avoid confusion with output such as console.log()

The first one gets executed when resolve is called in the executor function. The second callback function is executed when reject is called in the executor function.

Not if the Promise is already settled. In that case the callbacks will be scheduled independently of calls to resolve() and reject().

adds all the functions from the microtask queue into the call stack

This phrasing suggests piling functions in the call stack one on top of another. This doesn't make sense.

The above code creates a promise to generate a random number from 1 to 10

Nope. Even after you fix the typo Math.ceil(Math.random * 10); => Math.ceil(Math.random() * 10);, the result is 0 to 10, because Math.random() can return 0.

P.S.

In my previous comments I was heated after commenting on blatantly poor code ( dev.to/ilya_sher_prog/comment/m5f7 ) so the tone could be better.

Collapse
 
kabir4691 profile image
Kabir Nazir

Uptil now, I thought that arguments and parameters were the same. But I just looked them up on the web again and understand the difference now. Shall correct it.

I used the word 'output' instead of 'result' because I didn't want the reader to confuse it with the 'result' property of the promise. I have even clarified what I mean by output in the next sentence, to be more clear.

Regarding the execution of the callbacks, I didn't mention about the condition you spoke of, because I wanted to keep this article simple without diving too much into the specifics. However, I did add this paragraph, which should give the readers more clarity. Please let me know if it still doesn't clear any misconceptions. I shall edit more, in that case.

Note that in a promise, the executor function can call only either resolve or reject once. Any subsequent calls to either resolve or reject after the first one are ignored. This is because a promise is supposed to have a single result of either success or failure.

Regarding the call stack, I was indeed of the idea that functions do get added and piled into the call stack from the microtask queue. After all, that's why it's called a stack, right? Please correct me if I am wrong, hopefully with some online resources to clear my concepts.

You're right about the random function. I was initially of the impression that the value returned is from 0 to 1, both exclusive. I shall correct that as well as the typo.

P.S.
I apologize for losing my cool as well in the reply to your comment. I understand that you don't want articles with half baked information floating around on the Internet that can confuse beginners. But, you also need to understand, that as a beginner, everyone is bound to make mistakes. When that happens, it's up to senior devs like you to correct and guide the junior ones. If there was no room for making mistakes, then platforms like Dev and Medium wouldn't exist in the first place. All that would exist is MDN or some other official documentation and no one else would be allowed to discuss concepts on the web. In the pursuit of ensuring quality, one must take care not to be too harsh on newbies that it totally discourages them from writing at all. Hope you understand.

Collapse
 
ilya_sher_prog profile image
Ilya Sher

I thought that arguments and parameters were the same

You are welcome!

I used the word 'output' instead of 'result' because I didn't want the reader to confuse it with the 'result' property

I see. Not sure what would be best here. output can be confused with I/O.

Regarding the execution of the callbacks, I didn't mention about the condition you spoke of, because I wanted to keep this article simple without diving too much into the specifics.

I see... but this phrasing suggest that resolve() must happen after .then(...) is attached. This is incorrect.

functions do get added and piled into the call stack from the microtask queue.

I am not checking the source code but chances are call stack is used like in many other languages meaning it contains call frames (created by calling a function and removed by returning from a function) one on top of each other, not arbitrary unrelated code. This should be checked.

You're right about the random function. I was initially of the impression that the value returned is from 0 to 1, both exclusive. I shall correct that as well as the typo.

I would rewrite the random example because despite the 0 to 10 range, the chances of getting exactly zero are very slim. That's why code using random numbers typically floors and not ceils the numbers.

floating around on the Internet that can confuse beginners

Huge concern. Later, I need to work with these people.

But, you also need to understand, that as a beginner, everyone is bound to make mistakes.

That's optimistic. Not only as beginner.

senior devs like you to correct and guide the junior ones

In my perfect word, that happens somehow in an organized manner before publication to avoid people learning from possibly incorrect un-reviewed content. How realistic is that? Probably not that much.

discuss concepts on the web

Mmm.. discussion is fine but articles here are presented as information (I guess assumed by beginners to be correct), not as discussion.

harsh on newbies

Just making it clear: nothing particular against newbies :) Just correctness. You can see I opened issues on GitHub for Rust book and C++ documentation at Microsoft. I have no idea who did that but probably not beginners.

Thread Thread
 
ilya_sher_prog profile image
Ilya Sher • Edited

I am not checking the source code

Did check. It had reference to html.spec.whatwg.org/multipage/web...

The relevant part is:


While the event loop's microtask queue is not empty:

  • Let oldestMicrotask be the result of dequeuing from the event loop's microtask queue.
  • Set the event loop's currently running task to oldestMicrotask.
  • Run oldestMicrotask.
  • Set the event loop's currently running task back to null.

In light of this, you should also review your previous articles in series because the call stack is mentioned all over there.

Thread Thread
 
kabir4691 profile image
Kabir Nazir

but this phrasing suggest that resolve() must happen after .then(...) is attached

That wasn't how the article was phrased. I did attach code examples below it to demonstrate it better. However, I have still edited the post to make it more clearer.

I would rewrite the random example

I agree. I shall edit accordingly.

call stack is used like in many other languages meaning it contains call frames (created by calling a function and removed by returning from a function)

I'm aware of the concept of call frames. But if I had to mention that in my previous articles, I would have to go even much deeper and that could make the post very long. My aim of the articles was to give a basic but sound explanation of concepts without diving too much into advanced concepts or specifics.

articles here are presented as information (I guess assumed by beginners to be correct), not as discussion.

I think it's safe to say that it should be common knowledge for readers that articles on Dev.to are written by a myriad of people (ranging from beginner to experienced) and not by the people who actually developed the technologies. Hence, while the articles can be used as a source of information, they shouldn't be necessarily taken as a gospel of truth. The official documentation is always where any developer should consult first for precise and deep knowledge.

The same goes for other corrections you have mentioned in my post. Getting into all the details for a specific topic isn't what Dev or Medium is about. This is a blogging website, not an official documentation source. But I do understand your concern regarding the correctness of the information. What I can do is add a link at the bottom of the post redirecting the user to the official documentation (like MDN), where they can explore more about the concepts in detail.

Collapse
 
ilya_sher_prog profile image
Info Comment hidden by post author - thread only accessible via permalink
Ilya Sher

Please stop confusing people

A Promise is a special type of Javascript object which indicates the eventual completion or failure of an asynchronous function.

"A Promise is a proxy for a value not necessarily known when the promise is created." -- MDN

Much better. Just go and read there - developer.mozilla.org/en-US/docs/W...

callback functions that are attached to it

WAT?

What can be attached to Promise are handlers: then and catch, see same MDN link.

"new Promise(executor) - A function that is passed with the arguments resolve and reject." -- this one is correct. Go and read there - developer.mozilla.org/en-US/docs/W...

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
ilya_sher_prog profile image
Info Comment hidden by post author - thread only accessible via permalink
Ilya Sher

Please stop assuming what people did or did not do.

My line 'A Promise is a special type of Javascript object which indicates the eventual completion or failure of an asynchronous function.' is literally the first line in the same MDN document

Factually incorrect. The first line says

The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.

You rephrased it using a much less precise and confusing language.

which you obviously haven't already

Assumption.

I see you're a fan of reading documents, be it on Dev.to or MDN, incompletely.

Assumption.

Some comments have been hidden by the post's author - find out more