Hello. Studying JavaScript (and, in principle, any other technology), various questions always arise, the main of which is: âWhy does it work this way and not otherwise?â And it is very important at this moment not only to find the answer to the question, but also the explanation received embed in a single system of already acquired knowledge. Otherwise, the orphaned information will have to be memorized or forgotten.
Learning something together helps a lot to find answers. When a student / companion asks a question about how to understand the phrase - "... the result of the previous one" falls into the next promise in the chain ... "- you involuntarily think about it ... That's what a strange thing. But you canât say better anymore, is it really not clear? You look into the clean, slightly naive, companionâs eyes and understand - you need to say something else. It is desirable so that you do not even have to memorize. To just new information organically fit into existing human thoughts.
I will not describe what we tried, read, watched. As a result, we became interested in the ECMAScript specification. How to read and understand it is a separate conversation (maybe even a separate post). But the way promises and their behavior are described there, for the first time gave us a holistic and logically coherent understanding of this topic. What I want to share with you.
This article is for beginners. Promises in terms of the ECMAScript specification will be discussed here. I know it sounds strange, but as it is.
The promise object: its philosophy, technical presentation, possible states
It has been repeatedly noted by me that high-quality programming training should consist of 2 parts. This is a philosophical understanding of the idea, and only then its technical implementation. That is, ordinary human logic, which the student is guided by when making any decisions, greatly facilitates the understanding of the technical implementation of this decision. Therefore, we begin with what a promise is in life, and how do we relate to it? And then we'll see: how examples of promises will be implemented in code. Consider the following figures (Fig. 1, 2, 3).
fig 1. ([[PromiseState]] - as a result of a promise)
Figure 2. ([[PromiseResult]] - as information related to the result of a fulfilled or unfulfilled promise)
fig 3. ([[[PromiseFulfillReactions]], [[PromiseRejectReactions]] - as the consequences that occur after the fulfillment or failure to fulfill the promise)
We see that the very concept of promise stands on 3 pillars. 1) Was the promise fulfilled at all? 2) What additional information can we extract after fulfilling or refusing a promise? 3) What are the consequences if our promise is positive or negative?
Technically, a promise is an ordinary entity expressed through a data type such as an object. This entity has a Promise name / class. Objects born from this class have Promise.prototype in their prototype chain. And this entity must somehow be connected with all the âinformation from lifeâ that we examined above. The ECMAScript specification lays this information in a promise even at a level that is lower in abstraction than JavaScript itself. For example, at the level of C ++. Accordingly, in the object of the promise there is a place both under the status, and under the result, and under the consequences of the promise. Take a look at what ECMAScript's promise consists of (Figure 4).
Figure 4. (Internal fields of the promise object according to the ECMAScript specification)
What new colors did the phrase âpromise does not mean to marryâ play in terms of a programmer? 1) [[PromiseState]]. Someone has not married. 2) [[PromiseResult]]. Because he did not have enough money for the wedding. 3) [[PromiseRejectReactions]]. As a result, he got a lot of free time that he spent on self-development 4) [[PromiseFulfillReactions]]. Why does a person need plan B when he has already chosen plan A?
Yes, there is a fifth field [[PromiseIsHandled]]. It is not very important for us people, and we will no longer operate on it in the future. In short: there is stored a signal to the interpreter about whether the programmer rejected / rejected the promise or not. If not, the raw promise reject is interpreted by the JS engine as an error. For impatient ones: if the programmer did not hang the Promise.prototype.then () function as the callback handler of the rejected promise status, then the ârejectedâ promise state of the object will show you a red error in the developer's console.
Have you noticed that the fields of the promise object are enclosed in "[[" and "]]"? This emphasizes that the JS programmer does not have direct access to this information. Only through special tools / commands / API, such as the Promise.prototype.then () command. If you have an irresistible desire to manage âthis kitchenâ directly, then welcome to the EcmaScript specification standards club.
A short remark at the end of this sub-chapter. If in life we ââcan have promises partially fulfilled, then in EcmaScript - no. That is, if a person promised to give a million, but gave 950 thousand, then in life he may be a reliable partner, but for JavaScript such a debtor will be blacklisted through [[PromiseState]] === ârejectedâ. A Promise object changes its state unambiguously and only once. How this is technically implemented is a little later.
Designer Promise, his philosophy. The callback function executor is like the âexecutorâ of a promise. Interaction scheme: Promise (constructor) - executor (callback) - promise (object)
So, we found out that promise is an entity that is technically a JS object with special hidden internal fields, which in turn provide philosophical filling with the meaning of the word âpromiseâ.
When a newcomer creates a promise object for the first time, the following picture awaits him (Fig. 5).
Figure 5. (The very first time we intuitively create a promise object)
What went wrong and why the error is a standard question. When answering it, it is better to bring some analogy from life again. For example, few people like "empty chimes" around us: who only promise, but do nothing to fulfill their statements (politics does not count). We are much better at those people who, after their promise, have a plan and immediately take some action to achieve the promised result.
So the ECMAScript philosophy implies that if you create a promise, then immediately indicate how you will fulfill it. The programmer needs to draw up his plan of action in the form of a function parameter, which you pass to the Promise constructor. The next experiment looks like this (Fig. 6).
Figure 6. (Create a promise object by passing the executor function to the Promise constructor)
From the caption to the figure, we see that the function (Promise constructor parameter) has its own name - executor. Her task is to start fulfilling the promise and, preferably, bring it to some kind of logical conclusion. And if the programmer can write any code in the executor, then how can the programmer signal to JS that everything is done - can you go and see the results of the promise?
Markers or signals that help the programmer to inform that the promise has been completed are passed automatically to the executor parameters in the form of arguments specially formed by JavaScript. These parameters can be called as you like, but most often you will meet them under names such as res and rej. In the ECMAScript specification, their full name is resolve function and reject function. These function markers have their own characteristics, which we will consider a little later.
To understand the new information, the newcomer is invited to independently encode the following statement: "I promise that I can divide one number into another and give an answer, if only the divisor is not zero." Here's what the code would look like (fig. 7).
Figure 7. (Solution of the problem of dividing 2 numbers through promises)
Now you can analyze the result. We see that for the second time the browser console shows the Promis object in an interesting way. Namely: 2 additional fields are indicated in double square brackets. You can safely draw an analogy between [[PromiseState]] and [[PromiseStatus]], fulfilled and resolved, [[PromiseValue]] and [[PromiseResult]]. Yes, the browser itself tries to tell the programmer about the presence and value of the internal fields of the promise object. We also see the connected system of the promise object, the executor function, the special callback function-markers res and rej.
In order for the student / partner to become more relaxed in this material, he is offered the following code (Fig. 8). It is necessary to analyze it and answer the following questions.
Figure 8. (Variation of the solution to the problem of dividing 2 numbers through promises)
Will the code work? Where is the executor function here and what is its name? Is the name "wantToDivide" appropriate in this code? What does the bind function return after itself? Why are arguments passed to the bind function only in second and third place? Where did the special functions resolve function and reject function disappear? How did the necessary input numbers number1 and number2 get into the "promise fulfillment plan"? How many elements are in the arguments pseudo-array? Is it possible to recover from memory how the answer will look in the browser console?
The reader is invited to think about the answers to the questions himself. And
experiment in code. Fortunately, the code is small and the idea of ââthe task is simple. Yes, there are questions about both promises and general knowledge of JavaScript. What to do, everywhere we are waiting for surprises that prevent us from relaxing. As soon as everything becomes clear to you, you can move on.
let number1 = Number(prompt("input number 1")); let number2 = Number(prompt("input number 2")); let wantToDivide = function() { if (arguments[1] === 0) { arguments[3]("it is forbidden to divide by zero"); return; } let result = arguments[0] / arguments[1]; arguments[2](result); }; let myPromise = new Promise(wantToDivide.bind(null, number1, number2)); console.log(myPromise);
Consider executor-a arguments: resolve and reject functions
So, we had a coffee - we move on. Let us consider in more detail the special functions resolve function and reject function, which are automatically generated by JavaScript to translate the promise of the object into the fulfilled or rejected state, which symbolizes the end of the promise.
For starters, let's try to look at them simply in the developer's console (Fig. 9).
Figure 9. (Investigation of the function resolve function - res)
We see that the resolve function is a function that takes one argument (property length === 1). And its prototype is Function.prototype.
Ok, let's continue the experiments. And what will happen if we remove the link to the resolve / reject function from the executor to the external scope? Will something break (fig. 10)?
Figure 10. (We translate myPromise promise into a fulfilled state outside the promise)
Nothing unusual. Functions as a subspecies of an object in JavaScript are passed by reference. Everything worked out as we expected. The variable from the outerRes closure got a reference to our resolving function res. And we used its functionality to put the promise in the fulfilled state outside the executor itself. The following slightly modified example shows the same thought, so look at the code and think about what state and what value myPromise1 and myPromise2 will be in (Fig. 11). Then you can check your assumptions under the spoiler.
Figure 11. (The task of reflection. In what state and with what value will the promises myPromise1 and myPromise2 be in the developer's console?)
Figure 12. (The answer to the problem in Figure 11)
And now you can think about one interesting question. But how does the resolve / reject function always know exactly which promise to translate the object into the necessary state? We turn to the algorithm in the specification , which describes how these functions are created (Fig. 13).
Figure 13. (Features of creating resolving functions for one specific promise object)
Important points to pay attention to:
- at the time of creation of the resolve / reject functions, they are rigidly attached to the only promise object corresponding to it
- resolve / reject functions as an object data type have their own hidden [[Promise]] and [[AlreadyResolved]] fields, which provide everyone with the familiar intuitive logic that a) - resolving functions themselves translate the promise object into the necessary state; and the fact that b) a promise cannot be transferred to another state if at least once a resolve or reject function was called on it. This algorithm can be represented by the following figure (Fig. 14).
Figure 14. (Hidden function fields of resolve function and reject function)
Algorithms that use this information from hidden fields will not be considered now, since they are verbose and more complex. We still need to prepare for them both theoretically and morally. For now, I just can confirm your thought: âWow, how simple it turns out. Probably, at each resolution / resolution of the promise of the object, the âobjectâ flag {[[Value]]: false} will be checked. And if it is set to true, we stop the process of translating the promise into another state with a simple return. " Yes - thatâs exactly what happens. It seems that you can correctly answer the following question without problems. What will be the result in the developer's console (Fig. 15)?
Figure 15. (An experiment showing the relationship between resolve and reject functions with one specific promise object)
Algorithm for creating a promise object according to the ECMAScript specification
Consider the bewitching moment when it is born into the world - a full-fledged promise object (Fig. 16).
Figure 16. (Algorithm for creating a promise object from the EcmaScript specification)
No complicated questions should arise when viewing it:
- Promise constructor must be called in constructor mode, and not just a function call
- Promise constructor requires an executor function
- create a javascript object with specific hidden fields
- initialize hidden fields with some initial values
- create the resolve and reject functions associated with the promise object
- we call the executor function for execution, passing there already generated tokens resolve function and reject function as arguments
- if during the execution of executor-a something went wrong, put our promise object in the rejected state
- return to the variable the born promise promise object.
I donât know if it was a discovery for you that the function executor algorithm is executed here and now, in normal synchronous mode, even before something is written to the variable to the left of the Promise constructor. But in due time for me it became a revelation.
Since we have already touched upon the topic of synchronism and asynchrony, here is the following code for you to âthink aboutâ or for experimentation. Question: Having looked at some creation of the programmer Dima, can you answer what is the meaning of the game encoded below?
function randomInteger(min, max) { return Math.floor(min + Math.random() * (max + 1 - min)); } function game() { let guessCubeNumber = Number(prompt("Throw dice? Guess number?", 3)); console.log("throwing dice ... wait until it stop"); let gameState = new Promise(function(res, rej) { setTimeout(function() { let gottenNumberDice = randomInteger(1, 6); gottenNumberDice === guessCubeNumber ? res("you win!") : rej(`you loose. ${gottenNumberDice} points dropped on dice`); }, 3000); }); return gameState; } console.log(game());
Of course, this is an emulation of a die roll. Can the user guess the number that fell out or not? See how organically asynchronous setTimeout integrates into the synchronous executor - in our plan, roll a die and find out the number that fell out. How can one interpret the results in the developer console in a special way (Fig. 17)?
If we try to see the promise before the cube stops (3000 ms is indicated in the code), then we will see that the promise is still in a waiting state: the game has not finished, the cube has not stopped, there is no number dropped out. If we try to see the promise object after the cube stops, we will see very specific information: whether the user won (guessing the number), or lost and why (what number actually fell).
Figure 17. (The promise state of an object when there is an asynchronous operation in the executor function)
If you are interested in this example, or if you want to guess the dropped number of a flipped cube, you can copy the code and conduct your experiments. Dare!
Promise reaction as a consequence of a fulfilled promise
As you can see in Figure 14, the consequences of resolving / resolving a promise of an object are signed as â+ reactionâ and â-reactionâ. The official term for these words from the ECMAScript specification is promise reaction. It is expected that in future articles this topic will be discussed in detail. For now, we confine ourselves to the general idea of ââwhat a promise reaction is, so that this term can be correctly associated with the philosophical meaning of this word and its technical execution.
As we recall, a promise may have consequences, but may not. What is the consequence? This is an action that will happen some time later: after the promise is fulfilled. And since this is an action, the consequence can be expressed by a normal JavaScript function. Some functions will be executed in case of a successful resolving of the promise (+ reaction); other functions - in the case when the promise goes into the rejected state (-reaction). Technically, these functions (consequences) are passed in arguments when the Promise.prototype.then () method is called.
Thus, an important part of a promise reaction is an asynchronous action that is performed sometime in the future. There is a second important part of the promise reaction - this is the newly created promise returned after the Promise.prototype.then () command is executed. This is because the consequences affect other promises. For example, there is a promise to buy a car, but only after the promise is fulfilled to earn a certain amount of money. One promise was fulfilled - the consequence worked out - now the second can be fulfilled.
In fact, a promise reaction binds promises to each other in a certain time interval. It is important to remember that reaction is processed automatically. Function calls - the consequences of resolving a promise - are made by the JS engine, not the programmer (Fig. 18). And, since the reactions are closely related to the promise objects (promises) themselves, it is logical to assume that the promise reaction algorithms use their internal fields in their logic. And it is better to know about all these nuances in order to be able to consciously control asynchronous logic built on promises.
Figure 18. (The consequences of resolving a promise are recorded by callback functions in the then () method. The callback will be called asynchronously automatically by the JS engine)
To summarize
1) We got acquainted with the promises in JavaScript, their philosophy and technical execution. All this is implemented using special internal promise fields of the object: [[PromiseState]], [[PromiseValue]], [[PromiseFulFillReactions]], [[PromiseRejectReactions]].
2) The programmer is given the opportunity to fulfill his promise through the executor function, passed as an argument to the Promise constructor.
3) The boundaries of a fulfilled or unfulfilled promise are determined by special marker-functions, resolve function and reject function, often in the code called res and rej. These functions are automatically created by JavaScript and passed arguments to the executor.
4) resolve function and reject function always have a promise object associated with them, as well as a common special field {[[Value]]: false}, which ensures that the promise is resolved only once.
5) [[PromiseFulFillReactions]] and [[PromiseRejectReactions]] are internal fields of the promise object that store the consequences of resolving the promise, an important part of which are custom asynchronous functions defined through the Promise.prototype.then () promise method of the object.
PS
This article has been prepared as an abstract of the video session of the InSimpleWords group. There are enough such âvideo lessonsâ and there is still material for taking notes. Another question is whether it will be interesting for community members to read what article about promises in a row. We are waiting for your comments.
- Promise constructor must be called in constructor mode, and not just a function call