Menu Home

Promisefying Legacy Callbacks

Using Promises can help you get a handle on all that asynchronous mess that older “callback” type functions gave you. And yet while there has been great leaps in retrofitting code to use promises (I’m thinking Node fs module here), many of us are left out there adapting code written back in the stone ages, with callbacks galore.

Here is a very simple, elegant way I use to adapt old code.

The problem

Let’s say you’ve run across this code:

// get product mapping
var selectedProduct = "jumpercables";
$.ajax({
  url: "/productmapping.json", 
  success: function (data, textStatus, jqXHR) {
    var products = JSON.parse(data);
    var product = products[selectedProduct];
    $.ajax({
      url: "/products.json", 
      data: {id: product.id}, 
      success: function (data, textStatus, jqXHR) {
        var productInfo = JSON.parse(data);
        displayOnBrowserScreen(productInfo);
      },
      error: function (jqXHR, textStatus, errorThrown) {
        displayError("We got an error getting product " + selectedProduct + ": " + errorThrown);
      }
    });
  },
  error: function (jqXHR, textStatus, errorThrown) {
        displayError("We got an error getting product " + selectedProduct + ": " + errorThrown);
  })
});

(Yes, I *know* jQuery handles promises much better than above. But I’ve seen a LOT of this kind of code. So – you know – chill.)

As you can tell, the above code is difficult to read.

Promisfying the above is very simple to do with just a few lines of code and placing everything into its own function. Accomplishing that simple means wrapping a Promise around the function. We resolve it on the success, and reject upon the error.

In the example above, we know we have six functions – two “success”, two “error”, and two “display” functions. We also know that the two “error” functions do the same thing, so in reality we have one “error” function. Therefore, we know we’re going to be writing five functions.

Here is the first one.

// get the product Mapping
const getProductMapping = (productSelected) => {
  return new Promise((resolve, reject) => {
    $.ajax({
      url: "/productmapping.json",
      success: function (data, textStatus, jqXHR) {
        resolve({productMapping: data, productSelected: productSelected});
      },
      error: function (jqXHR, textStatus, errorThrown) {
        reject(errorThrown);
      }
    });
  });
}

The above is much easier to read, as it deals with one function and one function only. There is no processing of the data – that is done by other functions. It’s sole purpose is to get the productMapping JSON data.

Having a function do one thing and one thing only is the key to maintainable code.

Here are the other functions. They follow the exact same design:

// get products
const getProducts = (data) => {
  const products = JSON.parse(data.productMapping);
  const product = products[data.productSelected];
  return new Promise((resolve, reject) => {
    $.ajax({
      url: "/products.json",
      data: {id: product.id},
      success: function (data, textStatus, jqXHR) {
          resolve(data);
      },
      error: function (jqXHR, textStatus, errorThrown) {
        reject(errorThrown);
      }
    });
  });
}

// error handler
const gotError = (errorText) => {
  displayError(errorText);
}
// display handler
const allGood = (data) => {
  const productInfo = JSON.parse(data);
  displayOnBrowserScreen(productInfo);
  return Promise.resolve(true);
}

Notice the last two functions. “gotError” displays an error message and, as you will see, will be the last function – no need for a Promise there. “allGood” will never throw an error (we hope!) so we will always return a resolved Promise.

Finally, there is the magic that ties it all together:

const selectedProduct = "jumpercables";
getProductMapping(selectedProduct)
  .then(getProducts)
  .then(allGood)
  .catch(gotError);

And that is it. There is nothing else. Much simpler code with which to deal.

Since we only resolve the Promise upon the successful AJAX call, we can be certain that the functions “getProductMapping”, “getProducts”, and “allGood” will always be executed in that order, and that printing will not occur until everything else is done. If there is a problem, then we reject the Promise and “gotError” is executed.

As you can see, the code is now much cleaner than before, and much, much easier to work with.

Here is the code in its entirety.

// get the product Mapping
const getProductMapping = (productSelected) => {
  return new Promise((resolve, reject) => {
    $.ajax({
      url: "/productmapping.json",
      success: function (data, textStatus, jqXHR) {
        resolve({productMapping: data, productSelected: productSelected});
      },
      error: function (jqXHR, textStatus, errorThrown) {
        reject(errorThrown);
      }
    });
  });
}

// get products
const getProducts = (data) => {
  const products = JSON.parse(data.productMapping);
  const product = products[data.productSelected];
  return new Promise((resolve, reject) => {
    $.ajax({
      url: "/products.json",
      data: {id: product.id},
      success: function (data, textStatus, jqXHR) {
          resolve(data);
      },
      error: function (jqXHR, textStatus, errorThrown) {
        reject(errorThrown);
      }
    });
  });
}

// error handler
const gotError = (errorText) => {
  displayError(errorText);
}

// display handler
const allGood = (data) => {
  const productInfo = JSON.parse(data);
  displayOnBrowserScreen(productInfo);
  return Promise.resolve(true);
}

const selectedProduct = "jumpercables";
getProductMapping(selectedProduct)
  .then(getProducts)
  .then(allGood)
  .catch(gotError);

Wrap-up

There’s a lot we can do to make the code above even better. The code above is just a demonstration and is NOT meant to be production code (you know who you are!).

And I know what you are thinking – there is more code than before. Yes, there is. Ideally, I would have each function in its own module and use something like Webpack to put everything together. That way, if all I’m working on is the “getProductMapping” function, then that’s all I see. It still technically means more code, but the trade-off is maintainability and TIME.

Let me know what you think.

Categories: Javascript

Tagged as:

thevirtuoid

Web Tinkerer. No, not like Tinkerbell.

Creator of the game Virtuoid. Boring JavaScript. Visit us at thevirtuoid.com

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: