2/01/2020

Javascript Promise - My Personal Best Practice

I just love promises. I do not think I could handle asynchronous operations without them. For me they present a simple way to abstract from parallel execution, so the code looks sequential to the programmer, which is a concept that is much easier to comprehend for the human mind - or at least mine.

This article assumes that you have a basic understanding of promises already. There are plenty of good tutorials out there, so I will focus on my personal take on best practices.

Let us look at a dead simple Promise and how its result can be handled:

new Promise((fulfill, reject) =>
{
  fulfill("hello world");
})
.then(
  (value) => { console.log("fulfilled", value); },
(message) => { console.log("rejected", message); }
)
.catch((error) => { console.log("error", error)})
.finally(() => { console.log ("finally")})
;
// console output:
// fulfilled hello world
// finally

This Promise, for the sake of a simple example, just fulfills immediately.
 
You can handle both, fulfillment and rejection in a single then-clause. I often see examples that use "then" to handle fulfillment and "catch" to handle rejection. This is semantically not correct, so do avoid this pattern. I will explain the difference later.

The catch-clause handles and unforeseen errors, such as trying to work with a null object. If not handled in a preceding then-clause, the catch-clause will also handle rejection. In this case, however, you will not know, whether there was a real error, which you better handle in your code, or if it was a "clean" rejection.

Finally, the finally-clause will be executed regardless of whether the Promise has been fulfilled or rejected. It will, mind you, only execute, after the Promise has been settled, i.e. was fulfilled or rejected. As long as the Promise is pending, the finally-clause will not execute.

N.B.: when I talk about "clauses" here, it just helps me to conceptualize the behaviour, I am aware that these are methods that take callbacks as their arguments. You may find another concept more helpful to streamline your own thoughts :-)

Now let us introduce a run-time error into the fulfillment handler:

new Promise((fulfill, reject) =>
{
  fulfill("hello world");
})
.then(
  (value) => { console.log("fulfilled", value); x = null.x; },
(message) => { console.log("rejected", message); }
)
.catch((error) => { console.log("error", error)})
.finally(() => { console.log ("finally")})
;
// console output:
// fulfilled hello world
// error TypeError: "null has no properties"
// [with a reference to your code]
// finally

As you can see in the output, the catch handler is invoked, and the error message can be logged. If you fail to do so, troubleshooting your code may soon become a nightmare.

I will not mention the finally handler, as it will always be called. Boring :-)


Now, moving the catch handler before the then handler makes a big difference:

new Promise((fulfill, reject) =>
{
  fulfill("hello world");
})
.catch((error) => { console.log("error", error)})
.then(
  (value) => { console.log("fulfilled", value); x = null.x; },
(message) => { console.log("rejected", message); }
)
.finally(() => { console.log ("finally")})
;
// console output:
// fulfilled hello world
// TypeError: null has no properties
// [with a clickable reference to your code]
// finally

Now the catch handler will handle any run-time errors preceding the then handler. Errors in the then handler itself, however, will not be handled, and thus will be automatically logged to the console as errors. That is helpful for two reasons: it does not matter, should you forget to display the error in the catch handler, and it gives you a clickable code reference to the origin of the error, so you can see the code and call stack. Nice :-)

Not to miss out on rejection, we will cover this as well:

new Promise((fulfill, reject) =>
{
  reject("hello world");
})
.then(
  (value) => { console.log("fulfilled", value); },
(message) => { console.log("rejected", message); }
)
.catch((error) => { console.log("error", error)})
.finally(() => { console.log ("finally")})
;
// console output:
// rejected hello world
// finally

And last but not least, this is an example that I often see in tutorials, which makes me mad:

new Promise((fulfill, reject) =>
{
    reject("hello world");
})
.then(
    (value) => { console.log("fulfilled", value); }
)
.catch((error) => { console.log("error", error)})
.finally(() => { console.log ("finally")})
;
// console output:
// error hello world
// finally

Because this relies on the catch handler to handle rejects, your code will be at a loss whether there was and error or a "clean" rejection. DO NOT DO THIS!

To summarize my personal best practice:
  1. use then() to handle bot, fulfillment and rejection
  2. put  catch() before then()

These two tiny details make life so much easier :-)

No comments:

adaxas Web Directory