Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Mark Drew and Michael Hnat
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Mark Drew Michael Hnat

Mysterious Error Handling Behavior With Proxied Futures In ColdFusion 2018

By
Published in Comments (20)

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.

Want to use code from this post? Check out the license.

Reader Comments

15,848 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.

15,848 Comments

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.

96 Comments

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

15,848 Comments

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

27 Comments

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

15,848 Comments

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

27 Comments

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.

15,848 Comments

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

15,848 Comments

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

15,848 Comments

@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 );
15,848 Comments

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

27 Comments

Ben,

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

15,848 Comments

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

27 Comments

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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel