Mysterious Error Handling Behavior With Proxied Futures In ColdFusion 2018
As I've been digging into the new Future functionality in ColdFusion 2018, I've stumbled over a number of caveats. But, this morning, I've run into something that I can't seem to make heads-or-tails of. From what I think I can demonstrate, the error handling capabilities in a Future chain appear to change drastically depending on whether or not the Future was generated directly; or, if the Future was generated by a ColdFusion user defined function (UDF).
To see what I mean, let's look at the following code. Here, I'm using a test() method to proxy the creation of a Future. Essentially, I am using the test() method to execute the runAsync() method for me:
<cfscript>
public any function test() {
// NOTE: This function is doing NOTHING BUT calling runAsync() and returning the
// resultant Future object. Essentially, this function is just a proxy for the
// execution of runAsync().
return runAsync(
function() {
var inner = runAsync(
function() {
sleep( 2000 );
},
100 // <------ THIS WILL TIMEOUT!!!
);
return( inner.get() );
}
);
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
future = test()
.then(
function() {
return( "then-value" );
}
).error(
function() {
return( "error-value" );
}
)
;
writeOutput( future.get() );
</cfscript>
As you can see, the test() method is doing nothing but executing and returning the result of the runAsync() function. It just so happens that the runAsync() function will end in an error - but the details of that error are not the point of this post. What we want to focus on here is the error handling!
Now, we'd expect that outer runAsync() function error to be handled by the .error() callback in the calling context. But, when we run this code, we get the following ColdFusion output:
As you can see, the .error() method in the page context was not used to "catch" the bubbled-up timeout error (despite the fact that it was nested inside a second runAsync() call). Instead, ColdFusion treated this Task timeout error as an uncaught exception.
OK, here's where it gets totally confusing. Now, all we're going to do is take that runAsync() method and move it into the calling context. Essentially, we're going to unwrap the text() proxy:
<cfscript>
public any function test() {
// This method is no longer be used - the runAsync() was fork-lifted up to the
// calling context. It was moved AS IS with NO OTHER MODIFICATIONS.
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
future = runAsync(
function() {
var inner = runAsync(
function() {
sleep( 2000 );
},
100 // <------ THIS WILL TIMEOUT!!!
);
return( inner.get() );
}
)
.then(
function() {
return( "then-value" );
}
).error(
function() {
return( "error-value" );
}
)
;
writeOutput( future.get() );
</cfscript>
As you can see, all we did was move the outer runAsync() call out of the test() method proxy and up into the calling context. This is - functionally-speaking - 100% the same exact code! And yet, when we run this modified version, we get the following ColdFusion output:
error-value
This time, the .error() callback was able to catch the runAsync() error.
But, nothing changed! All we did was take the runAsync() function and execute it directly rather than executing it inside of a logicless proxy function.
Am I missing something here? I've been staring at this code all morning and I just can't figure out what is going on. Hopefully someone else can see this and tell me where I am going wrong? Or what poor assumption I am making. I feel like I must be missing something obvious.
Want to use code from this post? Check out the license.
Reader Comments
@All,
It's even easier to see the oddities here if you pass the
Future
through anecho()
method:When we do nothing else but pass the
runAsync()
result through theecho()
method, it breaks the error-handling.If this is really the way error handling works -- and I'm not just missing something obvious -- then this is likely going to be the biggest hampering of the Future functionality. It essentially means you could never create access methods that return Futures; otherwise the calling context won't be able to use the
.error()
bindings.I believe in the first implementation you are returning the outer future anonymously with test() { return async().. } but never actually calling outers' .get method.
In the second implementation you are assigning runAsync as a property of test with
future = runAsync()
and the calling future.get() ...As such it's not exactly the same implementation... ( imo ) :)
@Edward,
Take a look at one of the comments I left though -- you can "break" the error handling by simply passing the
runAsync()
response through anecho()
method. I think that more clearly demonstrates the problem since theecho()
method should not change the functionality.Ben,
This is a bug, it will surface when you have proxied method playing runasync() . We will fix this.
@Vijay,
Ok cool-at least I'm not going crazy :)
@Ben,
Can you tell me the real world usecase wherein you would want to have a proxy access method for runasync and why you would not use runasync directly there.
@Vijay,
The most obvious one to me is that you may have Gateway / Service methods encapsulate the logic of making some sort of request. So, for example, imagine you have a "remote API" to call. You could have Gateway methods like this:
This would now allow you to consume the remote API either as a blocking call:
... or, as a non-blocking, asynchronous call:
However, in the latter case, the calling context cannot really "leverage" the Future in a natural way since the
.error()
won't catch the error properly.Essentially, this completely eliminates a whole swath of encapsulation / abstraction choices.
Thanks Ben for providing me the example. Yes, it's completely design choice as to how you want to model your apis and workflows. But for getThingById(id) kind of API, sync version will make more sense since you want result to be returned immediately or else it should fail with an error/exception. If you choose to go async then you will anyway have to do get() on future and return the result which would be a blocking call. Let me know your views.
@Vijay,
Also, what if you want to try to build more async functionality around the concept of Futures. So, forget about the "remote API" -- what if I wanted to build something like this:
... where I might want to take an Array of Futures and wait till all of them are resolved in parallel (as opposed to in serial using
.then()
). This is whatPromise.all()
does.Then, the concept of "Racing":
... where I want to use the first fulfillment and ignore the rest. Again, coming from the Promise world, there a lot of
Promise.race()
implementations.In each of these cases, we don't care how or why the Future objects are being created -- we're just trying to build functionality around managing those futures. But, if we encapsulate that logic (which would likely be complicated logic) inside a Function, then we lose the ability to use the returned Future in a natural way.
@Ben,
Again, the underlying thought here is that if you don't allow proxies Futures to behave like Futures, then you limit the creative ways in which people can use Futures.
But, to be fair, I know nothing about how Futures work in Java. I only know Promises in JavaScript. So, maybe there are a lot of limitations in Java that make this kind of stuff difficult.
Thanks Ben. I got your usecase. I will evaluate this and get back to you.
@Vijay,
Groovy. It's also possible that Futures are not really intended to be "composed". After all, there is still a lot of value in simply being able to run a few methods in parallel. So, I don't want to sound like I am diminishing the work. Honestly, at the end of the day, if I can do:
... oops, hit ENTER too soon.
... Honestly, at the end of the day, if I can do:
... that's still awesome and will have huge performance implications :D
I'm just trying to dig in and see all the fun things that we can try to do with this.
Ben,
I really appreciate the different usecases you are bringing up, this will really help us improvise the stuff further. Thanks a bunch.
@Vijay,
Woot woot, #TeamWork :D
@Vijay,
I've tried to put together a more interesting demonstrate of why I might want to create a Future inside a Function and then return that Future and have the
.error()
method work "as expected":www.bennadel.com/blog/3495-experiment-creating-a-promise-inspired-future-constructor-in-coldfusion-2018.htm
Hopefully this kind of post sheds some light on where my brain is going.
Thanks Ben, sounds good. I will check this out.
@All,
Ok, so I found an even more crazy issue. Which may actually be the root-cause of the issue in this "proxy method" post. It turns out, the very act of storing a Future in an intermediary variable will break error handling:
www.bennadel.com/blog/3496-saving-a-future-in-an-intermediary-variable-breaks-error-handling-in-coldfusion-2018.htm
So, it's very possible that what is breaking the proxy-method is the fact that the Future gets stored in a method-argument? At this point, I wouldn't know how to test these two errors independently.
Hi Ben,
Greetings and Good News!!
We have done some recent changes to ColdFusion 2018 Async Framework - the first level then(), error() and their corresponding timed versions are unblocking now. Your use case for showing error details as cause to the terminal exception and proxy usage have also been solved. We have also taken care of Timeout exception to be caught by error(). It should be available as a part of ColdFusion 2018 update.
I will keep you posted on the update details.