Managing Asynchronous Resources in JavaScript

Published

The adoption of Promises has made asynchronous programming much more accessible in JavaScript web development. While it sometimes is the perfect solution, extra checks must be built into any components that use asynchronous resources or data (how can we guarantee they will be available?).

Due to the load order of JavaScript on a given site, I sometimes find that components reliant on asynchronous resources actually initialize before the scripts responsible for loading the async resources. The problem can be exacerbated when the loading these resources occurs in obscure areas of the codebase; areas that I’m not keen on updating due to risk of impacting other sites.

Depending on how much access I have to code base, there are three different options I’ve used to handle these cases:

  1. JavaScript setInterval – poll for a resource’s availability on an interval, which is great when you don’t have an object reference in that you can bind to.
  2. Promise Deferred Pattern – immediately setting a then-able Promise on the window, and resolving the Promise later from outside of its initialization.
  3. ES6 Proxy Object – Define an Object to manage references to asynchronous resources and fire events to notify downstream components when an asynchronous resource becomes available.

1. The JavaScript setInterval

The most basic, yet versatile, method is to poll using an Interval, and not proceed with code execution until the resource becomes available:

// definte an interval and save the reference into a variable
let magnificPopupLoadInterval = setInterval(() => {

  // test for presence of resource each time callback executes 
  if ($.magnificPopup) {
    
    // if resoure is found clear the interval
    clearInterval(magnificPopupLoadInterval)
    
    // execute code using asychronous resource
    $('.image-popup').magnificPopup()

  }
}, 125) // I've used intervals of 125ms and have not run into issues

Here is the same interval logic but wrapped in a then-able Promise:

// create a Promise that resolves once resource is loaded
let magnificPopupLoadPromise = new Promise((resolve, reject) => {
  let magnificPopupLoadInterval = setInterval(() => {
  // test for presence of resource each time callback executes 
  if ($.magnificPopup) {
    // if resoure is found clear the interval and resolve the Promise
    clearInterval(magnificPopupLoadInterval)
    resolve()
  }
}, 125) // I've used intervals of 125ms and have not run into issues

// make use of then-able Promise 
magnificPopupLoadPromise.then( ()=> {
  // execute code using asychronous resource
  $('.image-popup').magnificPopup()
})

2. Promise Deferred Pattern

I’ve always been a fan of this pattern, which was made available by libraries like q and jQuery before the Promises specification existed. Unfortunately it has been deprecated out of browsers’ native Promise1, but that doesn’t mean we can’t use it.

The Deferred pattern is particularly useful when the logic to initialize a Promise is separate from the logic to resolve it. For example, if I need to define a Promise in one file, but resolve the promise in another file, the Deferred pattern allows me to do this. Caution, this may not meet best practices (e.g. violates JavaScript Pure Functions), but it’s still nice to have in the tool belt.

Below is an example of how the native JavaScript Promise can be used to create a custom Deferred Object (borrowed largely from this Stack Overflow post).

// create a Deferred class which allows external access to a Promise's resolve function
class Deferred {
  constructor() {
    this.promise = new Promise((resolve, reject)=> {
      this.reject = reject
      this.resolve = resolve
    })
  }
}

// create an instance of the Deferred Object and assign a callback when the Deferred Object is resolved
let deferredInstance = new Deferred()
deferredInstance.promise.then((result) => {
  console.log('the Deferred resolved with a result of: ' + result)
})

// you are now free to resolve the Deferred Object at any time, any where it is referencable
setTimeout(()=> {
  deferredInstance.resolve(42)
}, 500)

Here another example of the Deferred Pattern using jQuery’s Deferred Object:

// create a jQuery Deferred Object
var deferred = $.Deferred();

// execute an asychronous task
$.ajax({
  url: 'https://example.com/getEvents',
  success: function(events) {
      // once the task completes, resolve the Deferred Object; keep in mind this can be done
      //  any where that the original Deferred Object is referencable (e.g. in another function
      //  or file)
      deferred.resolve(events);
  }
});

// attach a callback to the Deferred Object, to be executed once it has resolved
deferred.then(function(events) {
  console.log('async task to get events data completed and returned:', events)
})

3. ES6 Proxy Object

Now for a bit of fun using a new feature introduced in ES6: the Proxy Class. The Proxy Class wraps around a JavaScript Object and controls access to the Object’s properties. The Proxy wrapper prevents direct access to the underlying Object, and instead executes user-defined callback functions. For those with a Java background, I like to think of it as extending an existing JavaScript Object, and overriding certain functions (or traps as they’re referred to in this context).

There are many great uses for Proxy Objects, but here we will be using it to provides consumers of asychronous resources two things:

  1. A location they can check to see if a resource is available
  2. An event they can listen for, which fires once the resource does become available

Below is a code snippet defining the Proxy Object:

// define the Proxy Object, which accepts a target Object as the first parameter,
//  and then a config Object as the second parameter (defining the "traps")
window.asyncResources = new Proxy({}, {
  set: function (target, key, value) {
    // set value to underlying object to allow retreival later
    target[key] = value
    // dispatch custom JavaScript event to notify consumers that resource is now available
    let evt = document.createEvent('Event')
    evt.resource = value
    evt.initEvent('asyncResources.available.' + key, true, false)
    window.dispatchEvent(evt)
  }
})

As we can see, the asyncResources Object is attached to the window and accessible from anywhere throughout our application. As resources become available, we can set them as a properties on this Object, and they too will become accessible throughout the application.

The secondary function of our Proxy Object is to emit a custom JavaScript event whenever new resources become available, alerting any interested components that “hey, the resource is now available.” Components can be configured to listen by attaching an event listener to the window like so:

// listen for the custom event emitted by our Proxy object above
window.addEventListener('asyncResources.available.resourceA', (e) => {
  // execute any resource dependent code now that the resource is available
  console.log('asyncResources.resourceA event fired', e.resource)
})

Now that we have the asyncResources Proxy Object defined, and the consuming components configured to listen, we can test things out by setting a property on the asyncResources Object. Our event listener above is listening for a property name “resourceA”, which we can set using this snippet:

asyncResources.resourceA = {
    name: 'taylor',
    age: 29,
    likes: ['biking','hiking','programming']
}

When setting the resourceA property, I see the console log message from our component event handler. Our component now knows that resourceA is available, and free to do what it needs with the asynchronous resource.

Screen capture of the Google Chrome JavaScript debugger console showing that when we set the resourceA property on the asyncResources Object, our component event handler above fires property.

Inspiration for Writing this Post

At work I sometimes inherit codebases that are dependent on in-house libraries that only load resources as needed. The best example is the jQuery Magnific Popup plugin – our JavaScript will query the DOM looking for the Magnific Popup class, and only import the JS if that class is found.

Sounds great right? But what happens when later you’re tasked with building another component that’s dependent on the lightbox modal functionality, say a popup video player. You now need to build logic into the video player to check and make sure the lightbox resource has loaded before trying to use it.

References

  1. https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred