Skip to content

Instantly share code, notes, and snippets.

@connesc
Last active August 29, 2015 14:23
Show Gist options
  • Save connesc/10d54fcba54e3dbaf2c3 to your computer and use it in GitHub Desktop.
Save connesc/10d54fcba54e3dbaf2c3 to your computer and use it in GitHub Desktop.
Asynchronous promise handling with Bluebird

Asynchronous promise handling with Bluebird

This Gist serve as an illustration for a Stack Overflow thread. It contains a real situation, where a Bluebird promise needs to receive its handler asynchronously.

Context

The goal is to launch a long process, which writes a preview to stdout upon startup. The launchProcess() function should launch the process and return once the preview has been read. In particular, it should not wait for the completion of the process.

As an example, you would could think to a large file download (written to the disk), with the HTTP headers written to stdout as soon as they are received.

Files

  • launchProcess.js: the initial and intuitive implementation, which triggers a Bluebird warning about a possible unhandled rejection (false positive).
  • launchProcess_workaround.js: the same as above, but with a workaround to avoid the false positive. Although more compact, this is also less intuitive in my opinion.
  • client.js: a possible quick & dirty client to illustrate how launchProcess() could be called.
launchProcess.then(onLaunched, console.error);
function onLaunched(result) {
console.log('Preview:', result.preview);
result.completion.then(console.log, console.error);
})
var child_process = require('child_process');
var Promise = require('bluebird');
module.exports = exports = launchProcess;
/*
* Launch a long process and wait for its startup to be completed.
*/
function launchProcess() {
var process = child_process.spawn("command");
var startup = readPreview(process.stdout);
var completion = watchProcess(process);
return startup
.finally(checkError)
.then(buildResult);
/*
* If the process encountered an error during the startup, propragate it.
*/
function checkError() {
if (completion.isRejected()) {
return completion;
}
}
/*
* Build the resulting object.
*/
function buildResult(preview) {
return {
preview: preview,
completion: completion
};
}
}
/*
* Read the preview which is outputted by the process during its startup.
*/
function readPreview(stream) {
return new Promise(function resolver(resolve, reject) {
// Consume the beginning of the stream, until the whole preview has
// been read. Resolve or reject the promise accordingly.
});
}
/*
* Build a promise representing the process completion.
*/
function watchProcess(process) {
return new Promise(function resolver(resolve, reject) {
// Basically:
process.on('error', reject);
process.on('exit', resolve);
// A non-zero exit code, a killing signal and/or a non-empty stderr
// may also be considered as errors.
});
}
var child_process = require('child_process');
var Promise = require('bluebird');
module.exports = exports = launchProcess;
/*
* Launch a long process and wait for its startup to be completed.
*/
function launchProcess() {
var process = child_process.spawn("command");
var startup = readPreview(process.stdout);
var completion = watchProcess(process);
return new Promise(function resolver(resolve, reject) {
completion.catch(reject);
startup.then(buildResult).then(resolve, reject);
});
/*
* Build the resulting object.
*/
function buildResult(preview) {
return {
preview: preview,
completion: completion
};
}
}
/*
* Read the preview which is outputted by the process during its startup.
*/
function readPreview(stream) {
return new Promise(function resolver(resolve, reject) {
// Consume the beginning of the stream, until the whole preview has
// been read. Resolve or reject the promise accordingly.
});
}
/*
* Build a promise representing the process completion.
*/
function watchProcess(process) {
return new Promise(function resolver(resolve, reject) {
// Basically:
process.on('error', reject);
process.on('exit', resolve);
// A non-zero exit code, a killing signal and/or a non-empty stderr
// may also be considered as errors.
});
}
@benjamingr
Copy link

You have a race condition based on whether or not completion is already rejected/resolved when you check for isRejected, if it rejects after startup completed its rejection will be suppressed if I'm reading this correctly.

@connesc
Copy link
Author

connesc commented Jun 25, 2015

The goal of checkError is to prevent an erroneous preview from being returned if it is known that the process failed.
What's more, if the process fails early, there are good chances that both promises get rejected. In that case, checkError allows to keep the only relevant rejection (the process failure).

But maybe I don't see your point?

@bergus
Copy link

bergus commented Jun 25, 2015

Why so complicated? :-) Bluebird helper functions are your friends!

function launchProcess() {
    var process = child_process.spawn("command");

    var startup = readPreview(process.stdout);
    var completion = watchProcess(process);

    return Promise.race([completion, startup.then(function buildResult(preview) {
        return {
            preview: preview,
            completion: completion
        };
    })]);
}

Admittedly, race is not exactly what we want, because we only want to propagate errors and not cancel completion once startup is fulfilled.
I guess it would be better if readPreview would install an error handler on process, and watchProcess is only called from within buildResult.

@bergus
Copy link

bergus commented Jun 25, 2015

@benjamingr: No, that seems to be intentional. launchProcess() may fulfill with completion being rejected later, that's fine.

The problem I see is rather when completion fails before startup settles, becuase in that case we indeed do have an uncaught rejection. It should be possible to reject launchProcess() sooner if that happens (like the workaround.js does it explicitly).

The process api is not easy to consume indeed :-) It would be necessary for all the consumption of the stream to fail when the process fails, I don't think there is currently anything that ensures that.

@connesc
Copy link
Author

connesc commented Jun 25, 2015

@bergus, .race() is very nice. I come from Q, and I didn't noticed it yet. Thanks!

However, if completion gets resolver before startup (for some reasons), the wrong result may be resolved. I guess that .race() is primarily intended for homogeneous values.

Also, I don't get your point about the cancellation of completion. I cannot find any information about this in the API reference.

Regarding your suggestion for moving the error handler, it was also my first intention but it would mix semantically unrelated code.

@connesc
Copy link
Author

connesc commented Jun 26, 2015

@bergus, if completion fails before startup settles, then the rejection gets correctly transferred by checkError. But yes, the workaround is better here because the resulting promise is immediately rejected, without waiting for the settling of startup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment