Synchronous promise resolution (bluebird vs. jQuery)

24,693

Solution 1

Short version: I get why you want to do that, but the answer is no.

I think the underlying question being asked is whether a completed promise should immediately run a callback, if the promise has already completed. I can think of a lot of reasons that this might happen - for example, an asynchronous save procedure that only saves data if changes were made. It may be able to detect changes from the client side in a synchronous fashion without having to go through an external resource, but if changes are detected then and only then would an asynchronous operation be required.

In other environments that have asynchronous calls, the pattern seems to be that the developer is responsible for understanding that their work might complete immediately (for example, .NET framework's implementation of the async pattern accomodates this). This is not a design problem of the framework, it's the way it's implemented.

JavaScript's developers (and many of the commenters above) seem to have a different point of view on this, insisting that if something might be asynchronous, it must always be asynchronous. Whether this is "right" or not is immaterial - according to the specification I found at https://promisesaplus.com/, item 2.2.4 states that basically no callbacks can be called until you are out of what I'll refer to as "script code" or "user code"; that is, the specification says clearly that even if the promise is completed you can't invoke the callback immediately. I've checked a few other places and they either say nothing on the topic or agree with the original source. I don't know if https://promisesaplus.com/ could be considered a definitive source of information in this regard, but no other sources that I saw disagreed with it and it seems to be the most complete.

This limitation is somewhat arbitrary and I frankly prefer the .NET perspective on this one. I'll leave it up to others to decide if they consider it "bad code" to do something that might or might not be synchronous in a way that looks asynchronous.

Your actual question is whether or not Bluebird can be configured to do the non-JavaScript behavior. Performance-wise there may be a minor benefit to doing so, and in JavaScript anything's possible if you try hard enough, but as the Promise object becomes more ubiquitous across platforms you will see a shift to using it as a native component instead of custom written polyfills or libraries. As such, whatever the answer is today, reworking a promise in Bluebird is likely to cause you problems in the future, and your code should probably not be written to depend on or provide immediate resolution of a promise.

Solution 2

You might think this is a problem, because there's no way to have

getSomeText('first').then(print);
print('second');

and to have getSomeText "first" printed before "second" when the resolution is synchronous.

But I think you have a logic problem.

If your getSomeText function may be synchronous or asynchronous, depending on the context, then it shouldn't impact the order of execution. You use promises to ensure it's always the same. Having a variable order of execution would likely become a bug in your application.

Use

getSomeText('first') // may be synchronous using cast or asynchronous with ajax
.then(print)
.then(function(){ print('second') });

In both cases (synchronous with cast or asynchronous resolution), you'll have the correct execution order.

Note that having a function being sometimes synchronous and sometimes not isn't a weird or unlikely case (think about cache handling, or pooling). You just have to suppose it's asynchronous, and all will be always fine.

But asking the user of the API to precise with a boolean argument if he wants the operation to be asynchronous doesn't seem to make any sense if you don't leave the realm of JavaScript (i.e. if you don't use some native code).

Solution 3

The point of promises is to make asynchronous code easier, i.e. closer to what you feel when using synchronous code.

You're using synchronous code. Don't make it more complicated.

function print( text ){

    console.log( 'print -> %s', text );

    return text;
}

function getSomeTextSimpleCast( opt_text ){

    var text = opt_text || 'Some fancy text-value';

    return text;
}

print(getSomeTextSimpleCast('first'));
print('second');

And that should be the end of it.


If you want to keep the same asynchronous interface even though your code is synchronous, then you have to do it all the way.

getSomeTextSimpleCast('first')
    .then(print)
    .then(function() { print('second'); });

then gets your code out of the normal execution flow, because it's supposed to be asynchronous. Bluebird does it the right way there. A simple explanation of what it does:

function then(fn) {
    setTimeout(fn, 0);
}

Note that bluebird doesn't really do that, it's just to give you a simple example.

Try it!

then(function() {
    console.log('first');
});
console.log('second');

This will output the following:

second
first 

Solution 4

There are some good answers here already, but to sum up the crux of the matter very succinctly:

Having a promise (or other async API) that is sometimes asynchronous and sometimes synchronous is a bad thing.

You may think it's fine because the initial call to your API takes a boolean to switch off between sync/async. But what if that's buried in some wrapper code and the person using that code doesn't know about these shenanigans? They've just wound up with some unpreditable behavior through no fault of their own.

The bottom line: Don't try to do this. If you want synchronous behavior, don't return a promise.

With that, I'll leave you with this quotation from You Don't Know JS:

Another trust issue is being called "too early." In application-specific terms, this may actually involve being called before some critical task is complete. But more generally, the problem is evident in utilities that can either invoke the callback you provide now (synchronously), or later (asynchronously).

This nondeterminism around the sync-or-async behavior is almost always going to lead to very difficult to track down bugs. In some circles, the fictional insanity-inducing monster named Zalgo is used to describe the sync/async nightmares. "Don't release Zalgo!" is a common cry, and it leads to very sound advice: always invoke callbacks asynchronously, even if that's "right away" on the next turn of the event loop, so that all callbacks are predictably async.

Note: For more information on Zalgo, see Oren Golan's "Don't Release Zalgo!" (https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md) and Isaac Z. Schlueter's "Designing APIs for Asynchrony" (http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony).

Consider:

function result(data) {
    console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", result );
a++;`

Will this code print 0 (sync callback invocation) or 1 (async callback invocation)? Depends... on the conditions.

You can see just how quickly the unpredictability of Zalgo can threaten any JS program. So the silly-sounding "never release Zalgo" is actually incredibly common and solid advice. Always be asyncing.

Share:
24,693
thuld
Author by

thuld

JavaScript, C#, Dynamics 365, a bit of PowerShell

Updated on July 05, 2022

Comments

  • thuld
    thuld almost 2 years

    I have developed a small lib for the Dynamics CRM REST/ODATA webservice (CrmRestKit). The lib dependes on jQuery and utilizes the promise-pattern, repectivly the promise-like-pattern of jQuery.

    Now I like to port this lib to bluebird and remove the jQuery dependency. But I am facing a problem because bluebird does not support the synchronous resolution of promise-objects.

    Some context information:

    The API of the CrmRestKit excepts an optional parameter that defines if the web-service call should be performed in sync or async mode:

    CrmRestKit.Create( 'Account', { Name: "foobar" }, false ).then( function ( data ) {
       ....
    } );
    

    When you pass "true" or omit the last parameter, will the method created the record in sync. mode.

    Sometimes it is necessary to perform a operation in sync-mode, for instance you can write JavaScript code for Dynamics CRM that is involed for the save-event of an form and in this event-handler you need to perform sync-operation for validation (e.g. validate that a certain number of child-records exist, in case the right number of records exist, cancel the save-operation and show an error message).

    My problem now is the following: bluebird does not support the resolution in sync-mode. For instance when I do the following, the "then" handler is invoked in async fashion:

    function print( text ){
    
        console.log( 'print -> %s', text );
    
        return text;
    }
    
    ///
    /// 'Promise.cast' cast the given value to a trusted promise. 
    ///
    function getSomeTextSimpleCast( opt_text ){
    
        var text = opt_text || 'Some fancy text-value';
    
        return Promise.cast( text );
    }
    
    getSomeTextSimpleCast('first').then(print);
    print('second');
    

    The output is the following:

    print -> second
    print -> first
    

    I would expect that the "second" appears after the "first" because the promise is already resolved with an value. So I would assume that an then-event-handler is immediately invoked when applied on an already resolved promise-object.

    When I do the same (use then on an already resolved promise) with jQuery I will have my expected result:

    function jQueryResolved( opt_text ){
    
        var text = opt_text || 'jQuery-Test Value',
        dfd =  new $.Deferred();
    
        dfd.resolve(text);
    
            // return an already resolved promise
        return dfd.promise();
    }
    
    jQueryResolved('third').then(print);
    print('fourth');
    

    This will generate the following output:

    print -> third
    print -> fourth
    

    Is there a way to make bluebird work in the same fashion?

    Update: The provided code was just to illustrate the problem. The idea of the lib is: Regardless of the execution-mode (sync, async) the caller will always deal with an promise-object.

    Regarding "... asking the user... doesn't seems to make any sense": When you provide two methods "CreateAsync" and "CreateSync" it is also up to the user to decide how the operation is executed.

    Anyway with the current implementation the default behavior (last parameter is optional) is a async execution. So 99% of the code requires a promise-object, the optional parameter is only use for the 1% cases where you simply need a sync execution. Furthermore I developed to lib for myself and I use in 99,9999% of the case the async mode but I thought it is nice to have the option to go the sync-road as you like.

    But I thinks I got the point an sync method should simply return the value. For the next release (3.0) I will implement "CreateSync" and "CreateAsync".

    Thanks for your input.

    Update-2 My intension for the optional parameter was to ensure a consistend behavior AND prevent logic error. Assume your as a consumer of my methode "GetCurrentUserRoles" that uses lib. So the method will alway return an promise, that means you have to use the "then" method to execute code that depends on the result. So when some writes code like this, I agree it is totally wrong:

    var currentUserRoels = null;
    
    GetCurrentUserRoles().then(function(roles){
    
        currentUserRoels = roles;
    });
    
    if( currentUserRoels.indexOf('foobar') === -1 ){
    
        // ...
    }
    

    I agree that this code will break when the method "GetCurrentUserRoles" changes from sync to async.

    But I understand that this I not a good design, because the consumer should now that he deals with an async method.

  • Denys Séguret
    Denys Séguret over 10 years
    +1 because I think you got it right but I made another answer because I'm not sure your answer is really easy to understand from OP's point of view (I might be wrong of course).
  • thuld
    thuld over 10 years
    The point of the code was to have an example where a sync promise will result in "first" "second". My code should only show the frame of my problem, not the way I will use promises. You are right with your second code-example that promise will ensure that always result in the intendet print-order.
  • Denys Séguret
    Denys Séguret over 10 years
    The real important point is that having a code depending on a promise being immediately executed is a bug. Not having synchronous resolution of promises will help you avoid this kind of bug.
  • Brandon
    Brandon over 9 years
    @Florian Margaine To be honest I rather prefer the way jQuery handles 'then' synchronously. I use it for example to build a dynamic chain of events whereby subsequent functions rely on results from the previous, which also vary based on certain criteria, and is handled by a shared fail and done function. The alternative without promises usually results in a convoluted series of callback functions that are very hard to follow. Not knocking bb here, I am considering migrating for the performance benefits. I just don't understand how I would accomplish this otherwise if then is always async.
  • Benjamin Gruenbaum
    Benjamin Gruenbaum over 9 years
    @Brandon What are you talking about? If you have sync functions don't use promises - if you have async functions jQuery deferreds create race conditions by running asynchronously sometimes on the other hand Bluebird always runs the same way regardless of the race.
  • Brandon
    Brandon over 9 years
    It's a little hard to express without a code sample and I don't want to hijack the thread. Basically multiple dependent "then" functions against a promise which resolve once the chain of functions is arranged. like myPromise.then(functionA).then(dynamicFunc).then(functionD).‌​fail(failFunction).d‌​one(doneFunction) where dynamicFunc can be either functionB or functionC. then just calling myPromise.resolve({data}) to start. Each function defines a deferred and returns a promise (except done and fail) then resolves or rejects the deferred either passes data to the subsequent function or calls fail.
  • Brandon
    Brandon over 9 years
    BTW can you explain the "race condition" thing? Possibly with an example? I can't seem to find much info specifically about why that might happen. If you know any good resources I would love to see.
  • JLRishe
    JLRishe over 9 years
    @Brandon Well, for one, if you assumed that long string of actions was going to execute synchronously (and acted on that assumption in some way), and then part of it turned out to be asynchronous, you would be SOL. Any tiny performance benefit you get out of having it run synchronously is just premature optimization. As a side note, it's not necessary to return a new promise from a handler function that you pass to then(). If you return an ordinary value, it will be passed to the next handler just fine.
  • idbehold
    idbehold about 8 years
    Promises can only resolve or reject a single value. You could resolve the entire result object and then change the callback function provided to the .then() to only accept the result object as an argument.
  • Nicolas
    Nicolas about 8 years
    Thanks, seems this is the only way. I did modify the App's code to accept one object as parameter and extract from there the other two. I guess this is a breaking change when upgrading the library to a new version.
  • metalim
    metalim almost 8 years
    The problem with .then (and callback call delayed until next execution cycle) is that it adds significant delay in most implementations, making Promises unacceptable solution for time critical processes, like animations. For example, if you have something like: spinUp.then(rotateXtimes).then(slowDown), you'll get noticeable animation breaks, while synchronous resolution with simple callbacks like spinUp(function(){rotateXtimes(function(){slowDown(done)})}) does not introduce any delays.
  • pospi
    pospi over 7 years
    +1 for mentioning the troubles with this inconsistency, I was about to track down the Zalgo articles myself (;
  • LandonSchropp
    LandonSchropp over 7 years
    Thanks for posting this explanation. We just spent an hour on a failing spec before finding this. :(
  • Venryx
    Venryx about 7 years
    @DenysSéguret "The real important point is that having a code depending on a promise being immediately executed is a bug." It's not always a bug. There are use-cases for having synchronous resolution/execution of callbacks, for when the data is already available and you want it synchronously, but you want to reuse the existing async-capable code-path. (for example, my current use-case here: github.com/stacktracejs/stacktrace.js/issues/188)
  • trincot
    trincot almost 7 years
    Modifying bluebird to break the compliance with Promises/A+ specs is a bad idea. Think of the person who later will need to debug code you have written... and naturally assumes Promises/A+ compliance.