Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Bob Silverberg
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Bob Silverberg@elegant_chaos )

Mysterious Error Handling Behavior With Proxied Futures In ColdFusion 2018

By Ben Nadel on
Tags: ColdFusion

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:


 
 
 

 
 Proxied future errors don't seem to be catchable by .error() callbacks. 
 
 
 

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.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

@All,

It's even easier to see the oddities here if you pass the Future through an echo() method:

<cfscript>
	
	public any function echo( required any value ) {

		return( value );

	}

	future = echo(
			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>

When we do nothing else but pass the runAsync() result through the echo() method, it breaks the error-handling.

Reply to this Comment

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.

Reply to this Comment

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 ) :)

Reply to this Comment

@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 an echo() method. I think that more clearly demonstrates the problem since the echo() method should not change the functionality.

Reply to this Comment

Ben,

This is a bug, it will surface when you have proxied method playing runasync() . We will fix this.

Reply to this Comment

@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.

Reply to this Comment

@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:

public any function getThingByID( id ) {
	return( apiClient.getByID( id ) );
}

public any function getThingByIDAsync( id ) {
	return(
		runAsync() {
			return( getThingByID( id ) );
		}
	);
}

This would now allow you to consume the remote API either as a blocking call:

try {
	var thing = thingGateway.getThingByID( id );
} catch ( any error ) {
	// ....
}

... or, as a non-blocking, asynchronous call:

thingGateway.getThingByIDAsync( id )
	.then( ... )
	.error( ... )
;

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.

Reply to this Comment

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.

Reply to this Comment

@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:

var future = futureAll([
	getFutureA(),
	getFutureB(),
	getFutureC()
]).error(
	function() {
		// ....
	}
);

... 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 what Promise.all() does.

Then, the concept of "Racing":

var future = futureRace([
	getFutureA(),
	getFutureB(),
	getFutureC()
]).error(
	function() {
		// ....
	}
);

... 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.

Reply to this Comment

@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.

Reply to this Comment

@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:

var a = runAsync( getSomeData );
var a = runAsync( getSomeData );
Reply to this Comment

... oops, hit ENTER too soon.

... Honestly, at the end of the day, if I can do:

// Run these all in parallel !!
var a = runAsync( getSomeData );
var b = runAsync( getSomeData );
var c = runAsync( getSomeData );

var aData = a.get();
var bData = b.get();
var cData = b.get();

... 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.

Reply to this Comment

Ben,

I really appreciate the different usecases you are bringing up, this will really help us improvise the stuff further. Thanks a bunch.

Reply to this Comment

@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:

https://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.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.