When it comes to asyncronous operations in JavaScript there’s only one sane way to handle them, and that’s promises. The reason for this is that they’re composible:
all(asyncThingA(), asyncThingB())
.spread(function (resultA, resultB) {
});
The all function and the spread function are both really easy to build, and the above code simply says wait for both async operations to complete, then give me them. Writing the same thing using code based off event emitters or callbacks is a complete nightmare (I’m not even going to go into how you’d code that).
Promises are great for lots of things then, but there’s oen thing they haven’t yet got sorted out…progress. How on the earth should we handle progress?
Progress in event emitters
Event emitters have the upper hand when it comes to dealing with progress. They can just emit a progress event which you can handle.
function doAsyncOperation() {
var emitter = new Emitter();
var progress = 0;
setInterval(function () {
if (progress < 1)
emitter.emit('progress', progress);
progress += 0.1
}, 100);
setTimeout(function () {
emitter.emit('end');
}, 1000)
}
var op = doAsyncOperation();
op.on('progress', functoon (p) {
console.log('Operation is ' + (p * 100) + '% done.');
});
op.on('end', function () {
console.log('Operation complete');
});
This actually works great. It’s simple, easy to understand, and does what you need. There are two problems with adopting the same approach in promises:
Errors
What happens when an exception is thrown in a progress handler?
Just throw the error
With event emitters it will simply throw and bring down your application (browsers will carry on despite the error, but node.js apps won’t, and neither will windows 8 apps). That seems fine if you come from a world of event emitters, but jarring if your used to the promises behaviour of forwarding the error:
makePromise()
.then(funciton success(res) {
throw new Error('This error gets caught');
},
null,
function progress(p) {
throw new Error('This error crashes your app');
})
.then(null, function errorHandler(err) {
//handle error
})
.done();
The thing here is that it’s not as simple as saying “Never throw in progress handlers” because progress handlers aren’t really critical bits of code. You don’t really need them for your application. They might never get fired because your async operation might not support progress. Then you re-factor and decide to make the async operation support progress and your application crashes.
Ignore the error
You could just ignore the error. We’ve already said that progress handlers aren’t that critical to your application, so why bother notifying anyone of the exception? To answer that, imagine you’re developing an application for sharing images you have the perfect UX except for the fact that the progress bar doesn’t move. You spend hours trying to figure out what’s going wrong, only to find an error being silently ignored coming from your progress handler that would’ve made the problem obvious and easy to fix.
Never silently ignore an error unless it’s deliberate and acompanied by a note along the lines of “This error doesn’t matter because …”
Remaining options
So the remaining options are:
- Log the error, without throwing an exception
- Make the exception handleable as part of the promise
- Accept the problems with just throwing and just throw
None of these are completely un-acceptable, but none of them are ideal.
Composition
Parallel composition
What happens to progress in the example at the start of this post? Imagine the following:
AsyncThingA emits:
10ms Progress: 25%
20ms Progress: 50%
30ms Progress: 75%
40ms Progress: 100%
AsyncThingB emits:
30ms Progress: 25%
60ms Progress: 50%
90ms Progress: 75%
120ms Progress: 100%
If we just emit both sets of events we might get:
10ms Progress: 25%
20ms Progress: 50%
30ms Progress: 75%
30ms Progress: 25%
40ms Progress: 100%
60ms Progress: 50%
90ms Progress: 75%
120ms Progress: 100%
That’s not going to lead to a nice smooth progress bar. What if we take the average?
10ms Progress: 12.5%
20ms Progress: 25%
30ms Progress: 37.5%
30ms Progress: 50%
40ms Progress: 62.5%
60ms Progress: 75%
90ms Progress: 87.5%
120ms Progress: 100%
That’s more like it, it seems we have a winner. We have a problem if our progress isn’t represented as a percentage. Consider what would happen if we had:
10ms Progress: Stage A complete
20ms Progress: Stage B complete
Clearly the previous approach won’t work. So from that we need to either:
- Restrict progress values to a number between
0
and 1
- Support customised progress composition
Serial composition
What about when our promises are in serial rather than parallel:
doAsyncThingA()
.then(function (res) {
return doAsyncThingB(res);
})
Consider the functions are as they were in the parallel execution:
AsyncThingA emits:
10ms Progress: 25%
20ms Progress: 50%
30ms Progress: 75%
40ms Progress: 100%
AsyncThingB emits:
30ms Progress: 25%
60ms Progress: 50%
90ms Progress: 75%
120ms Progress: 100%
If we just pass all progress events straight through we might get:
10ms Progress: 25%
20ms Progress: 50%
30ms Progress: 75%
40ms Progress: 100%
70ms Progress: 25%
100ms Progress: 50%
130ms Progress: 75%
160ms Progress: 100%
That’s better than the parallel progress we had, but still pretty useless. In the same veign as before, we could assume they take roughly equal time and get the following:
10ms Progress: 12.5%
20ms Progress: 25%
30ms Progress: 37.5%
40ms Progress: 50%
70ms Progress: 62.5%
100ms Progress: 75%
130ms Progress: 87.5%
160ms Progress: 100%
It’s not going to be completely smooth, but it’s pretty good as an aproximation. Imagine we replace AsyncThingB with as syncronous operation of virtually 0 time. Our previous aproximation now yields:
10ms Progress: 12.5%
20ms Progress: 25%
30ms Progress: 37.5%
40ms Progress: 50%
40ms Progress: 100%
This is actually typically true of quite a lot of the operations in a promise chain.
A solution
This problem really needs solving, fast. Parallel composition is not too urgent, because there’s nothing else that doesn’t completely suck for parallel composition anyway. Serial composition needs to get fixed though. One option:
Take a progress-transformer
as the third argument to .then(cb, eb, progressTransformer)
. progress-transformer
can be one of three things:
false
- the value false
indicates that progress propogation should stop at this point.- a function - The progress values from the previous promise are passed to this function, propogation can be stopped by returning
false
, making false
an invalid value for progress. - Anything else - Anything else is also acceptable, and results in progress being propogated (effectively assuming that the current operation will be syncronous).
doAsyncOperationA()
.then(function (res) {
return doAsyncOperationB(res);
}, null, divideByTwo);
function divideByTwo(v) {
return v / 2;
}
Only the progress from operationA will be passed through the transformation, so the resulting promise yields:
10ms Progress: 12.5%
20ms Progress: 25%
30ms Progress: 37.5%
40ms Progress: 50%
70ms Progress: 62.5%
100ms Progress: 75%
130ms Progress: 87.5%
160ms Progress: 100%
Which is the best result we came up with for serial composition.
doAsyncOperationA()
.then(function (res) {
return doSyncOperationB(res);
});
Yields:
10ms Progress: 25%
20ms Progress: 50%
30ms Progress: 75%
40ms Progress: 100%
Which is perfect because it’s just the progress of A which is the same as the progress of the whole because B is sycronous.
A Solution to Parallel Composition
Parallel composition is a tricky one, I’d suggest that the all
function will have to do some extra work. If we let progress be any value, then the extra work should probably just be agregating all the progress into a big array. If we force progress to be a number, we should probably just take the average of that number.
API
The behaviour we have specified above should help to inform our decisions regarding the progress API. Having a progress handler as the third argument of .then
strongly implies that errors should be forwarded down the promise chain. Having it as a separate ‘event’ like structure would suggest it should just throw.
Option A
Make the third argument of .then
be the progress handler. Forward exceptions down the promise chain. Propogate all progress events if the argument is not a function and not false
. If the argument is false
, don’t propogate anything.
Option B
Separate out propagation compltely from handling. Essentially this lets us take the event-emitter aproach to handling progress.
A promise supports progress if it has a .progress
function.
If .progress
is called with an argument of type function
then it registers that function to recieve progress updates.
If .progress
is called with anything other than a function then all registered functions are called with that value.
If .progress(fn)
is called after .progress(val)
then fn
is called in the next tick with the most recent value.
Passing true as the third argument of .then
triggers basic propagation, but all other propagation must be done manually. The following two bits of code would therefore be equivalent:
var p2 = p1.then(cb, eb, true);
var p2 = p1.then(cb, eb);
p1.progress(p2.progress.bind(p2));
Conclusion
I’m actually in favour of Option B. It’s simple to impliment, and I feel that it probably covers the 80% case. By letting people manually write to the progress of a promise, they can artificially do whatever they want with progress, but we cover the case of one async operation that’s followed by a sync operation by just passing true as the third parameter.