Saturday 31 August 2019

Promise Reject vs Throw

Promises are designed to be resolved or rejected (by the code ran by the promise), but occasionally I've seen code that throws an exception rather than rejecting, so is there any difference? Well, sometimes yes and sometimes no. Let's say I have an "old school" callback based asynchronous function

function getPost(url, successFn){
 let posts = {
  "blog.fr/aaa": "AAA post"
 };
 //simulate async operation
 setTimeout(()=> successFn(posts[url]), 1000);
}

Now I have a function that makes use of it, adding some validation logic and "modernizing" it by returning a Promise.

function validateAndGetPostAsync(url){
 return new Promise((res, rej) => {
  if (!url)
   return rej("empty url");
  
  getPost(url, (post) => {
   if (!post)
    return rej("post not found");
   res(post);
  });
 });
}

It's rejecting the promise if the initial validation fails or if the returned post is null. Using it with await and a try-catch block works nicely:

let urls = ["blog.fr/aaa", null, "blog.fr/aaa"];
 for (let url of urls){
  try{
   console.log(`url: ${url} ${await validateAndGetPostAsync(url)}`);
  }
  catch (ex){
   console.log(`exception: ${ex}`);
  }
 }
 console.log("after retrieving blogs");
 
 //url: blog.fr/aaa - AAA post
 //exception: empty url
 //exception: post not found

Now, if rather than rejecting we use a throw:

function validateAndGetPostAsync2(url){
 return new Promise((res, rej) => {
  if (!url)
   throw "empty url";
  
  getPost(url, (post) => {
   if (!post)
    throw "post not found";
   res(post);
  });
 });
}

and run the same example as before:

 for (let url of urls){
  try{
   console.log(`url: ${url} - ${await validateAndGetPostAsync2(url)}`);
  }
  catch (ex){
   console.log(`exception: ${ex}`);
  }
 }
 console.log("after retrieving blogs");
  
 //Output:
 // url: blog.fr/aaa - AAA post
 // exception: empty url

 // D:\Main\MyPrograms\programs_JavaScript\promises\throwVsReject.js:31
         // throw "post not found";
         // ^
 //post not found

We can see that while the throwing in the initial validation is being caught, the throw in the validation after obtaining the post is not caught (node prints the exception and crashes, the "after retrieving blogs" line is never printed). So what's happening? Well, it's easy, validateAndGetPostAsync is calling getPost, that is an "old school" asynchronous function, it invokes a callback once it gets the post, rather than returning a Promise. This asynchronous callback runs in a different stack from the one that launched the code, so a try-catch block surrounding the callback won't catch the Exception. When invoking reject rather than throwing, this is not a problem. Why? Well, the await-try-catch is just magic that the compiler translates to a .then().catch() chain (more or less). A call to reject() obviously rejects the promise and is captured by the Promise.prototype.catch(). However, when doing a throw, the Promise constructor wraps in a try-catch block the call to the provided function, and that catch block will reject the promise. If the code runs outside that try (because it's running asynchronously in another stack) it won't be captured and no call to the reject callback will happen.

No comments:

Post a Comment