The .error() Method Cannot Catch Future Task Timeout Errors In ColdFusion 2018
Earlier this morning, I demonstrated that providing a Timeout value to a Future method will change the Future callback from an asynchronous call into a blocking, synchronous call in ColdFusion 2018. Now, I don't know if this is a related "issue"; but, it appears that the .error() method cannot catch Task Timeout errors thrown by preceding Futures in the same Future chain. This prevents complicates the use of the .error() method as a means to provide a fall-back value for a timed-out Task.
To demonstrate, we can use the sleep() command to simulate a hanging service. Then, we can attempt to catch the timeout error and provide a reasonable fall-back value in order to implement "graceful degradation":
<cfscript>
future = runAsync(
function() {
sleep( 3000 );
return( "Live data value" );
},
// Imagine this is some sort of CIRCUIT BREAKER and we want to make sure the
// callback doesn't just sit here and hang. So, we apply a timeout.
// --
// CAUTION: This will turn the above callback into a BLOCKING callback.
1000
).error(
function( required any error ) {
// The remote server may be failing or hanging. Let's provide a fall-back
// value for the Future.
// --
// CAUTION: THIS DOES NOT WORK - MAKE SURE TO KEEP READING THE POST.
return( "Fall-back data value." );
}
);
writeOutput( "Future value: " & future.get() );
</cfscript>
As you can see, the runAsync() callback will sleep for 3,000ms. And, the timeout is set to only 1,000ms. And, when we run this code, we get the following ColdFusion output:
Clearly, the .error() method was unable to catch and respond to the timeout of the runAsync() callback.
Now, given the fact that providing a timeout changes the runAsync() callback from an asynchronous call into a blocking, synchronous call, it does mean that we can provide a fall-back value using a traditional try/catch block:
<cfscript>
try {
future = runAsync(
function() {
sleep( 3000 );
return( "Live data value" );
},
// Imagine this is some sort of CIRCUIT BREAKER and we want to make sure the
// callback doesn't just sit here and hang. So, we apply a timeout.
// --
// CAUTION: This will turn the above callback into a BLOCKING callback.
1000
)
// By providing a "timeout" value to the runAsync() method above, it will turn the
// callback into a blocking, synchronous call. That means that errors thrown by the
// runAsync() method, including timeouts, can be caught in a normal try/catch block.
} catch ( any error ) {
// As a fall-back, create and fulfill and EmptyFuture.
future = runAsync();
future.complete( "Fall-back data value." );
}
writeOutput( "Future value: " & future.get() );
</cfscript>
In this case, rather than catching the error in a Future .error() method, we're creating and fulfilling an EmptyFuture in the Catch block. And, when we run this code, we get the following ColdFusion output:
Future value: Fall-back data value.
That works. But, coming from a Promise background, I really miss just being able to chain all of this stuff together. Luckily, we can nest runAsync() calls. Which means that we can move this blocking, synchronous control-flow into a nested runAsync() method. This will allow us to provide a timeout to the inner runAsync() call without forcing the outer runAsync() call into a synchronous control-flow:
<cfscript>
future = runAsync(
function() {
// While providing a timeout turns this runAsync() call into a BLOCKING,
// SYNCHRONOUS call, it is inside another runAsync() method, which is still
// executing asynchronously.
var nestedFuture = runAsync(
function() {
sleep( 3000 );
return( "Live data value" );
},
1000
);
return( nestedFuture.get() );
}
).error(
function( required any error ) {
// The remote server may be failing or hanging. Let's provide a fall-back
// value for the Future.
return( "Fall-back data value." );
}
);
writeOutput( "Future value: " & future.get() );
</cfscript>
As you can see, we moved the timeout value from the main Future chain into an inner Future chain. And, when we run this code, we get the following ColdFusion output:
Future value: Fall-back data value.
Groovy! That worked more like we would "expect" a Promise chain to work.
At first, it appears that the .error() method cannot catch Task Timeout errors. But, this is only true for timeouts in "same" Future chain. The .error() method can catch Task Timeout errors in nested Future chains. I can see that the Future data-type in ColdFusion 2018 is going to have a lot of caveats. But, I am hoping that once we understand the caveats, we can start to create some really powerful asynchronous control-flows.
Want to use code from this post? Check out the license.
Reader Comments
Ben,
We thought that throwing Timeout Exception rightway for the first level runasync call will make sense, so it was not being caught by error().
Based on your usecase of fall back logic we are thinking that it will make sense for error() to handle Timeout Exceptions at all level. This should come as a part of update.
@Vijay,
I'll, of course, defer to you guys. As long as it's documented, I think that's the important part. Otherwise, it takes a good deal of trial-and-error to understand how something is supposed to work.
That said, more than the need to provide a fall-back, I think having the
.error()
handle the timeout just makes the.error()
method more consistent.Yeah, that makes sense Ben.
@All,
So, I ran into something crazy this morning -- it seems like the error-handling behavior of a Future chain changes depending on whether or not the Future was generated directly; or, if it was generated inside a User Defined Function (UDF):
www.bennadel.com/blog/3494-mysterious-error-handling-behavior-with-proxied-futures-in-coldfusion-2018.htm
.... I am hoping that I just haven't had enough caffeine yet; and that someone can drop in and tell me what mistake I am making :D
@All,
Another super crazy issue -- it looks like storing Futures 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
I think I may be officially taking crazy-pills.