The Practical Client

Managing Asynchronous Processes in TypeScript

Promises not only provides a simple, flexible interface for assembling chains of asynchronous operations in client-side code, it also makes it easier for you to manage parallel processing.

In my last column, I introduced Promises and Promise.TypeScript as tools that provide a more standard way of handling asynchronous processing than callbacks.

But while it's nice to have a standard interface for asynchronous processing, the real issue with callbacks crops up when you have a sequence of asynchronous operations, each of which must execute only after the previous asynchronous operation has finished successfully. It's possible to do that with callbacks, but you'll be forced into increasingly more complicated levels of nested methods. Promises not only provides a simpler paradigm for composing chains of asynchronous processes, but also throws in tools for managing parallel processing.

Chaining Promises
In my previous column, I showed how to use the done method to perform processing when a Promise is resolved (that is, when the Promise returns its result). That's great…if you only want to do one thing. The Promise object's then method, however, allows you to create chains of additional processing and, unlike the done method, returns a Promise object holding the result of the method. The then method is the tool to use for composing chains of processes where each process not only needs to execute after the last process, but also needs the result from the previous property.

If the arrow expression passed to the then method returns a result (and the result isn't a Promise object), the then method wraps the result in a Promise and returns it; if the result is a Promise object, the then method just returns that Promise object. Both sets of the following code, for example, return a Promise object holding the Customer object returned by my GetCustomer method:

var pCust: Promise<Customer>;
pCust = GetCustomer( "A123" )
  .then( c => 
    { 
      return pCust.result; 
    });
pCust = GetCustomer( "A123" )
  .then( c => 
    { 
      return pCust; 
    });

Of course, that's no different from what the GetCustomer method does by itself. The power of the then method is in chaining together multiple methods and passing results among them. For example, I have a method called DeleteCustomer that accepts a Customer object, uses it to delete a row in the database, and returns a Boolean value indicating success or failure, wrapped inside a Promise object. To have my DeleteCustomer execute method as soon as GetCustomer completes (and to accept the Customer object returned by GetCustomer) I just need this code:

var pResult: Promise<boolean>;
pResult = GetCustomer( "A123" )
  .then( c => 
    { 
      return DeleteCustomer( c ) 
    });

The pResult Promise will be fulfilled when both the GetCustomer and DeleteCustomer methods are fulfilled. The pResult Promise will also hold the result of the last then method in the chain -- the one returned by the DeleteCustomer method.

Further, I also want to update my UI if both methods succeed. Here, again, the then method allows me to compose a chain of actions to execute as each part of the chain completes:

GetCustomer( "A123" )
  .then( c => 
    { 
      return DeleteCustomer( c ) 
    })
  .then( () => 
    {
      $( "#FullName" ).val( "Delete complete" );
    });

Because the final method doesn't need a parameter, I've omitted it. This code also doesn't catch the result of the final then method because, unlike my previous example, the final then method doesn't return a result.

But now I have two potential failure points: first, when I'm unable to retrieve the Customer object in GetCustomer; second, when I can't delete a successfully retrieved Customer in DeleteCustomer. However, because the format for all rejections is the same, I just need one fail method to handle both failures, provided I put the fail method at the end of my chain:

pSuccess = GetCustomer( "-1" )
  .then( c => 
    { 
      return DeleteCustomer( c ) 
    })
  .fail( r  => 
    { 
      $( "#FullName" ).val( r.message ); 
    });

A nice feature of the fail method is that it returns the Promise object produced by the previous item in the chain. That means, in this example, the fail method returns the Promise object returned from the DeleteCustomer method.

Parallel Processing
Of course, once you have asynchronous processing, you'll want to do parallel processing. Calling the GetCustomer method twice allows me to retrieve two customers in parallel:

var c1: Customer
var c2: Customer
GetCustomer( "A123" )
  .done( cRes => 
    { 
      c1 = cRes; 
    });

GetCustomer( "BC91" )
  .done( cRes => 
    { 
      c1 = cRes; 
    });

Of course, I then have to process the results of my two processes. I can use the when function of Promise to aggregate multiple Promise objects into an array that I can process. The when method accepts a collection of Promise objects and returns a Promise object with a result property holding an array of the results from the Promise objects it was passed. This example generates a Promise object holding an array of Customer objects (one from each GetCustomer method) in its result property. The following code then loops through the array:

pCust1 = GetCustomer( "A123" );
pCust2 = GetCustomer( "BASF" );

var pCusts: Promise<Customer[]>;
pCusts = P.when( pCust1, pCust2 );
pCusts.result
  .forEach( ( c ) => 
    { 
      alert( c.FullName ) 
    });

The when method's Promise object has, of course, a status property. That property will be set to Resolved when (and if) all the Promises passed to the when method resolve; the property is set to Rejected if any one of the Promises is rejected.

I really should be using that status property because I'm not guaranteed that my Promises will have been fulfilled when I process them. Fortunately, because the when method returns a Promise, I can use the then method with the when method. Using the then method lets me perform some action on the output from my multiple parallel processes after all of the parallel processes have resolved:

pCust1 = GetCustomer( "A123" );
pCust2 = GetCustomer( "BASF" );
P.when( pCust1, pCust2 )
  .then( () => 
    { 
      $( "#FullName" ).val( "All Results retrieved" ); 
    });

The arrow expression you use in the then method is passed the array of results so you can also process those results individually:

P.when( pCust, pCust2 )
  .then( ( pCusts ) => 
    { 
      pCusts.forEach( p => 
    { 
      alert(p.FullName) 
    }); });

Obviously, there's a wealth of functionality here and it's all bundled into a simple interface. If you're doing asynchronous processing in JavaScript, Promises is a tool that will dramatically simplify your life. I promise. You have my word on it.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube