WinJS.Promises – lessons learned
Goal of the promise is to bring the asynchronous programming into JS. It’s not new thing. You can find many information on wikipedia.
WinJS comes with built-in support of Promises. You can find more info on MSDN here.
Usage scenarios
- You call a native API which can take a long time. The call is started on UI thread. If it would take longer than 50ms and execute completely on UI thread, the user experience would be bad. The solution is to switch the execution to the background thread and later continue the work back on UI thread. In other words, API returns a promise object promising us that it will completed sometime later.
var openPicker = new Windows.Storage.Pickers.FileOpenPicker(); openPicker .pickSingleFileAsync() .then(function (file) { // do something with file });
- Splitting the work which runs on UI thread into several small chunks so the UI thread is not blocked. The result is that the whole execution time is longer but the application is more fluid, the UI thread is not blocked. It’s implemented via a call to setImmediate. This is very much used inside WinJS
- You build a library with a flexible API accepting an input which is not in your control. To make the API more robust, be prepared for longer data loading. In that case it’s recommended to use Promise and the consumer of that API can provide synchronous (fast case, 50ms) promise.
WinJS team did a great work and implemented built-in support of Promises for us.
In this article I’d like to summarize my personal experience with Promises, after using them for more than 1 year.
They have a state
They are built on the state machine. This means the promise has its internal state (i.e. created, working, completed, canceled ,error) and the methods which moves the state machine from state A to B. It’s good to keep it in mind, especially when you combine promises, i.e. chaining, joining promises, waiting for any promise.
Creating a promise
There are several ways how to do it:
- WinJS.Promise.wrap(value): it creates a completed promise with the result: value.
- WinJS.Promise.as(value): it checks if the value is promise and then returns it. Otherwise it’s same as WinJS.Promise.wrap(value). I use mainly this option.
- Calling new WinJS.Promise(completed, error, progress). The parameters are functions and none of them is required. If the promise is created, WinJS tries to complete it directly. If the promise is added to the chain (so other promises are in the chain before the created one) then the functions completed/error are called based on the previous promise result. Huh, starting to be complex, right? Yeap, but quite powerful.
var promise = new WinJS.Promise(function(complete, error, progress) { setTimeout(function() { // do some work complete(sum); }, 1000); });
Chaining the promises
It’s possible to chain the promises:
var promise = WinJS.Promise.timeout(1000) .then(function() { //do some work }) .then(function() { //do some work }) .then(null, function(error) { //error handler });
This means that when the first promise finishes, the next promise in the chain is executed.
Returning a value from promises
Any value returned from the promise is translated back into the completed promise with the return value.
var promise = WinJS.Promise.timeout(1000) .then(function() { return 1 + 1; }) .then(function(sum) { console.log(sum); // prints 2 }) .then(null, function(error) { //error handler });
Postponing the work, timeout-ing
Javascript developers are very used to use two ways how to postpone the work:
- msSetImmediate(function)
- setTimeout(function, milliseconds)
WinJS.Promises has the equivalent via calling one method: WinJS.Promises.timeout(milliseconds). If milliseconds are omitted or equal to 0, then msSetImmediate is used, otherwise setTimeout.
This is very useful technique when it’s necessary to execute CPU sensitive task while keeping UI responsive. It’s necessary to split the long CPU sensitive tasks into chunks and chain them with several timeouts (WinJS.Promise.timeout())
WinJS.Promise.as() .then(function () { // do some work (part #1) return WinJS.Promises.timeout(); }) .then(function () { // do some work (part #2) return WinJS.Promises.timeout(); }) .then(function () { // do some work (part #X) }) .done();
When timeout() is called, UI thread is able to process other tasks waiting in UI thread queue (i.e. user’s clicks, etc) and it keeps UI responsive.
Cancelling the promise
var p = WinJS.Promise.timeout(1000) .then(function () { return 1 + 1; }) .then(function (sum) { console.log("Sum is " + sum); }) .then(null, function (error) { //error handler }); console.log("Cancelling the promise"); p.cancel();
After call to cancel the promise, the promise finishes the current execution but stops the further promises in the chain execution. In the above example, “sum is 2” is not printed.
Failing the promise
There are several possible ways how to fail the promise:
- Returning the wrapped error promise
var p = new WinJS.Promise.wrap(10) .then(function () { return WinJS.Promise.wrapError("error promise"); }).then(null, function (error) { console.log("Promise error " + error + " occured but was handled"); });
- Throwing the exception
var p = new WinJS.Promise.wrap(10) .then(function () { throw "custom exception"; }).then(null, function (error) { console.log("Promise error " + error + " occured but was handled"); });
- Calling error callback
var promise = new WinJS.Promise(function (complete, error, progress) { if (shouldComplete) { complete(); } else { error("failing it"); } });
Failing the promise has the following consequences:
- when it’s the first level promise (it’s not nested promise done via join/any call), then failing the promise raises WinJS.Application.error event which by default terminates the application.
- the failed promise is in the error state, its returned value is null
- can be used to manage the execution flow in aggregating the promises via join/any (see bellow)
Aggregating the promises
It’s possible to create an aggregated promise which will continue when all promises (in case of join) or at least one of the promises (in case of any) are completed.
WinJS.Promise.join
There are two possible ways of passing the promises:
- Join the promises defined as object fields.
var promises = { p1: new WinJS.Promise.wrap(1), p2: new WinJS.Promise.wrap(2), p3: new WinJS.Promise.wrap(3), }; WinJS.Promise.join(promises).then(function (args) { console.log("p1: " + args.p1); // writes 1 console.log("p2: " + args.p2); // writes 2 console.log("p3: " + args.p3); // writes 3 });
- Join the promises defined as an array of the promises
var promises = [new WinJS.Promise.wrap(1), new WinJS.Promise.wrap(2), new WinJS.Promise.wrap(3)]; WinJS.Promise.join(promises).then(function (args) { console.log("0. promise: " + args[0]); // writes 1 console.log("1. promise: " + args[1]); // writes 2 console.log("2. promise: " + args[2]); // writes 3 });
As you can see above, the completed result of the joining promise is the input object/array with completed promises.
WinJS.Promise.any
There is also a second type of aggregated promise called “any” promise and it continues the execution when any of the input promises (object properties or array) are completed.
Managing the execution flow, passing the values
It’s possible to pass values using closure or using WinJS.Promise.join and passing the object with properties which contains promises or values. Let’s see it in action.
Example #1
Let’s say we are copying files (imagine promises p1, p2) and after all files were copied we want to check what was copied successfully. In the next sample p1 simulates first file failed, but we handled it. P1 result will be null, promise will be completed.
var p1 = new WinJS.Promise.as() .then(function () { console.log("Copying file P1"); throw "custom exception #1"; }).then(function (value) { console.log("P1 done"); return value; }, function () { //log error, cleanup if required }); var p2 = new WinJS.Promise.as() .then(function () { // copy file console.log("Copying file P2"); return {}; // simulating a handle object }).then(function (value) { console.log("P2 done"); return value; }, function () { //log error, cleanup if required }); WinJS.Promise.join({ p1: p1, p2: p2, m3: 3}) .then(function (args) { //args.p1 = null //args.p2 = promise with handle to file //args.m3 = 3 console.log("Joined promise completed"); }, function (error) { console.log("Joined promise error '" + error + "' occured but was handled"); }).done(); //output: Copying file P1 Copying file P2 P2 done Joined promise completed
Example #2
Let’s say we are copying files (imagine promises p1, p2) and after that we want to continue if ALL of the files are copied completely. In the next sample p1 simulates that copying first file failed so the whole promise failed. This means also joining failed.
var p1 = new WinJS.Promise.as() .then(function () { console.log("Copying file P1"); throw "custom exception #1"; }).then(function (value) { console.log("P1 done"); return value; }); var p2 = new WinJS.Promise.as() .then(function () { // copy file console.log("Copying file P2"); return {}; // simulating a handle object }).then(function (value) { console.log("P2 done"); return value; }); WinJS.Promise.join({ p1: p1, p2: p2, m3: 3}) .then(function (args) { //args.p1 = null //args.p2 = promise with handle to file //args.m3 = 3 console.log("Joined promise completed"); }, function (error) { console.log("Joined promise error '" + error.p1 + "' occured but was handled"); }).done(); //output: Copying file P1 Copying file P2 P2 done Joined promise error 'custom exception #1' occured but was handled
Notes:
- inner promises (p1, p2) are left to fail
- error object contains failed promises with error description
Logging the failed promises
It’s possible to get notified when any promise failed via attaching the listener to “error” event exposed on WinJS.Promise class.
function handlePromiseError(error) { console.log("PROMISE ERROR OCCURED " + (error.detail.exception ? error.detail.exception : "")); }; WinJS.Promise.addEventListener("error", handlePromiseError);
Finishing the promises UPDATE!
Promise chain can end with .then() or .done() call. But there is a difference. Based on the official documentation http://msdn.microsoft.com/en-us/library/windows/apps/hh700337.aspx the difference is that when the promise failed finishes with
- .then() – then the error is raised on WinJS.Promise.error and that’s all. The error is silently captured
- .done() – then the error is raised on WinJS.Promise.error and then there is a code
setImmediate(function () { throw value; });
which raises general application exception raised via WinJS.Application.error.
That’s clear. But the default promise usage/error handling within WinJS application is little bit different. When the application is started, call to WinJS.Application.start()
, there is a default promise error handler attached to WinJS.Promise.error
. This handler raises WinJS.Application.error
when the failed promise doesn’t have parent promise.
There is also a default error handler attached to WinJS.Application.error
which when called terminates the application by default.
Result of .done()/.then()
When you create WinJS application project and copy the samples from above MSDN link to your project, you find out, that the samples don’t work as described. The failed promises (both ending with then or done) terminates the application.
Reason: failed promise raises WinJS.Promise.error
event which is by default handled and in case the failed promise doesn’t have any parent, WinJS.Application.error
event is raised. Its event handler terminates the application.
Anyway, lessons learned: In general the best practice both for behavior and code readability is to use .done wherever you can and use .then wherever you have to.
Writing the promise to be readable
Chaining the promises can become very unreadable. My lessons learned are:
- You are chaining the promises and you want to return value. Don’t wrap value into the promise, return just the value. It gets automaticaly wrapped into the promise
- Don’t create inner chain the promises when not required and just chain them on the upper level
Bad style
p.then(function () { return asyncWork().then(function (result) { return moreWork().then(function (anotherResult) { return finishWork(); }); }); });
Good style
var p = WinJS.Promise.as(); p.then(function () { return asyncWork(); }).then(function (result) { return moreWork(); }).done(function (anotherResult) { return finishWork(); });
Chatching errors
Errors raised from the asynchronous code written using promises are hard to catch because the callstack doesn’t reflect where the error occurred but where how it was propagated in WinJS.Promise state machine. The recommended approach it turning on immediate catching JavaScript Runtime Exception in Visual Studio->Tools->Exception settings.
WinJS.Promise.as() .then(function () { throw "custom exception"; }).done(); Callstack: > terminateAppHandler [base.js] Line 6928 Script Application_errorHandler [base.js] Line 7097 Script dispatchOne [base.js] Line 6998 Script dispatchEvent [base.js] Line 6998 Script drainQueue [base.js] Line 7038 Script queueEvent [base.js] Line 7057 Script Anonymous function [base.js] Line 7190 Script Anonymous function [base.js] Line 7189 Script
Please note, there is no info where the exception actually occurred.
Thank you, this is helpful information to wrap my noodle around promises.
Thanks for the great article (actually its the best one on the internet covering WinJS promises)! But I have a question: how can I use .timeout() method to delay WinJS.xhr() call. (Use case: I want to see how my app looks like when REST api calls take long time and want to simulate slow api response). So far I tried `return WinJS.Promise.timeout(1000, WinJS.Promise.as(WinJS.xhr(options)));` but it returns failure. Thank you!