Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Gert Franz and Kevin Goldsmith
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Gert Franz Kevin Goldsmith

EXPERIMENT: Creating A Promise-Inspired Future Constructor In ColdFusion 2018

By
Published in Comments (8)

To be honest, when I saw that ColdFusion 2018 had "Futures", my first reaction was something like, "Awesome, we now have Promises in ColdFusion!" As I've dug into the feature, however, I've come to realize that ColdFusion Futures and JavaScript Promises have very little in common. At best, they both vaguely deal with asynchronous control-flow. But, that's essentially where the similarities end. Of course, that doesn't mean I can't try to shoehorn the Promise mental model into the Future abstraction in ColdFusion 2018. And, as an experiment, I wanted to see just how close I could come to creating a Promise-inspired constructor for Futures.

In JavaScript, the Promise() constructor takes a callback and returns a promise. The callback parameter of the constructor is invoked synchronously with two functions, resolve() and reject(). These two functions can then be called asynchronously in order to determine the outcome of the generated promise:

var promise = new Promise(
	function( resolve, reject ) {

		resolve( "I am the winner!" );

	}
);

promise.then(
	function( value ) {

		console.log( "Resolution:", value );

	}
);

This basic building block allows for a lot of flexibility in the asynchronous algorithms of JavaScript. So, I thought that if I could build something in the same vein in ColdFusion 2018, perhaps I could unlock a lot of asynchronous flexibility using Futures:

<cfscript>

	future = futureNew(
		function( resolve, reject ) {

			resolve( "I am the winner!" );

		}
	);

	future.then(
		function( value ) {

			writeLog( "Resolution: #value#" );

		}
	);

</cfscript>

This was a nice idea; but, in the current state of ColdFusion 2018, it feels very difficult to build on top of the Future abstraction - at least, that's how it feels with my level of programming skills. Right now, we have to overcome the following hurdles:

It feels a bit like the deck is stacked against me. But, I just couldn't let go of the dream. So, I kept hack and hacking until I came up with something that kind of, sort of, maybe resembles my original vision.

I offer up, futureNew():

<cfscript>

	/**
	* I invoke the given callback and return a Future. The callback is provided with two
	* methods: resolve() and reject(). Like a JavaScript Promise, resolve will result in
	* a successful outcome; reject will result in an error outcome. Errors thrown inside
	* of the callback will result in an automatic rejection.
	*
	* @callback I am the callback being used to initiate a Future.
	* @output false
	*/
	public any function futureNew(
		required function callback,
		numeric fulfillmentTimeout = ( 5 * 1000 )
		) {

		// CAUTION: Basically, anything that we do to a Future (like adding a Timeout or
		// calling the .then() or .error() methods) turns the Future into a BLOCKING,
		// SYNCHRONOUS call. As such, we need to double-nest our runAsync() call in order
		// to make sure that we don't accidentally block the top-level thread while
		// manipulating the inner thread.
		var asyncWrapper = runAsync(
			function() {

				// By calling runAsync() without a callback, we will receive an EMPTY
				// FUTURE. This is a Future that we can control. But, unfortunately, it
				// does not have a .then() method. Which means that we're going to have
				// to convert this EMPTY Future into a "normal" future before we return
				// it to the calling context.
				var bridge = runAsync();

				// I am the resolve function which will fulfill the Empty Future with a
				// successful outcome.
				var resolve = function( any value = "" ) {

					bridge.complete({
						value: value
					});

				};

				// I am the reject function which will fulfill the Empty Future with
				// an error outcome. This method expects either a STRING, a STRUCT, or an
				// actual Error object. If it's a STRING, it will be used as an Error
				// Message. And, if it's a STRUCT, it will be used as the internal
				// throw() attributes. Error objects will just be rethrown.
				var reject = function( required any error ) {

					bridge.complete({
						error: error
					});

				}

				var syncWrapper = runAsync(
					function() {

						callback( resolve, reject );

					}
				)
				.error( reject ) // Any uncaught error should become a rejection.
				.then(
					function() {

						// If the internal bridge EmptyFuture has not yet been fulfilled,
						// then let's sleep the Thread context for a small period of time
						// and check again.
						// --
						// NOTE: This won't block forever - the .then() timeout will
						// eventually throw an error and let the parent context proceed.
						while ( ! bridge.isDone() ) {

							sleep( 5 );

						}

						var resolution = bridge.get();

						// Handle the error outcomes.
						if ( resolution.keyExists( "error" ) ) {

							// NOTE: A this time, ColdFusion completely obfuscates the
							// errors that are created inside of a Future. As such, we're
							// going to write them to the log to make debugging half-way
							// possible for the developer.
							writeLog(
								type = "error",
								text = "FutureNew Error: #serializeJson( resolution.error )#"
							);

							// We're expecting the reject() function to be called with
							// either a string, a struct, or a native Error object. In
							// either case, we're going to try to convert the error value
							// into an actual ColdFusion Error object.
							if ( isSimpleValue( resolution.error ) ) {

								throw(
									type = "FutureNew.CustomException",
									message = resolution.error,
									detail = "The resolve() function was invoked with a simple value."
								);

							} else if ( isStruct( resolution.error ) ) {

								throw( attributeCollection = resolution.error );

							} else {

								// If it's not simple and not a struct, assume it is a
								// native error. In that case, just rethrow it.
								throw( resolution.error );

							}

						// If we made it this far, we are dealing with a successful
						// outcome. Now, it's possible that the value contains a Future.
						// In that case, we want to block and UNWRAP the Future before
						// passing it through.
						} else if ( isGettableFuture( resolution.value ) ) {

							return( resolution.value.get() );

						// Otherwise, let's just pass-through the resolution value.
						} else {

							return( resolution.value );

						}

					},
					fulfillmentTimeout
				);

				return( syncWrapper.get() );

			}
		);

		return( asyncWrapper );

	}


	/**
	* I determine if the given value is a Future object with a .get() method.
	*
	* @value I am the value being tested.
	* @output false
	*/
	public boolean function isGettableFuture( required any value ) {

		return(
			isInstanceOf( value, "coldfusion.runtime.async.Future" ) ||
			isInstanceOf( value, "coldfusion.runtime.async.EmptyFuture" )
		);

	}

</cfscript>

There's a lot of code to unpack here. But, the gist of it is that I'm using an EmptyFuture as a means to bridge the gap between the resolve() / reject() functions and a full then-able Future. Essentially, I have to block and wait until the EmptyFuture is resolved. Then, I have to use the results of that resolution in order to drive the outcome of an intermediary runAsync() callback.

Honestly, a lot of this code would have been greatly simplified if the EmptyFuture in ColdFusion 2018 could have .then() and .error() methods. Then, I wouldn't need to block and wait and a lot of this would become more natural.

That said, there's a lot of commenting in the code that I'll let you pick through on your own. Instead, let's jump right into looking at how the futureNew() function can be used:

<cfscript>

	// Include our FutureNew() function.
	include "./utilities.cfm";

	// FUTURE ISSUE: Because our FutureNew() method is encapsulating the creation of a
	// FUTURE object, it means that Future errors CANNOT BE HANDLED by the .error()
	// method. As such, we have to use a traditional Try/Catch to catch the error.
	// Hopefully, in the future (no pun intended), encapsulated Future errors will be
	// able to be dealt with via the .error() method.
	try {

		future = futureNew(
			function( resolve, reject ) {

				// NOTE: Using nested runAsync() methods to demonstrate that it's the
				// resolve() and reject() methods that control the futureNew() outcome.
				runAsync(
					function() {

						runAsync(
							function() {

								resolve( "So much win!" );

								// You can resolve with a Future - the Future will be
								// "unwrapped" implicitly by the FutureNew().
								resolve(
									runAsync(
										function() {

											return( "Inner zen!" );

										}
									)
								);

								// NOTE: The first call to resolve() or reject() will
								// cause the fulfillment. As such, having this here won't
								// do anything; unless we comment-out the resolve() lines
								// above.
								reject( "Such rejection!" );

								// If called with a Struct, the struct data will be
								// turned into a native ColdFusion error object.
								reject({
									type = "Boom",
									message = "This is crazy pants."
								});

								// NOTE: Normally, a throw() this DEEP in the nested
								// runAsync() calls wouldn't be noticed. It would be akin
								// to an "unhandled promise rejection". But, since we're
								// using a .error( reject ) below, then we can notice it.
								// --
								// Of course, this won't matter if the resolve() or
								// reject() methods have already been called.
								throw( type = "Kablamo!" );

								// If you're dealing with an error, the best approach is
								// to pass the error to reject(). That way, it will be
								// logged and you have a CHANCE of debugging.
								try {
									throw( "BoomGoesTheDynamite" );
								} catch ( any error ) {
									reject( error );
								}

							}
						)
						// When we have nested runAsync() calls, there's no way for the
						// FutureNew() method to know about unhandled errors. Unless, we
						// tie the reject() method into the unhandled error explicitly.
						// --
						// CAUTION: At this time, ColdFusion will basically HIDE ALL OF
						// THE ERROR DETAILS coming out of a Future. As such, you should
						// try to call the reject() function directly (either passing it
						// an Error object or a pseudo-error Struct). Then, at least it
						// will be logged to the ColdFusion logs.
						.error( reject );

					}
				)

			}
		).then(
			function( required string value ) {

				return( "Resolution: #value#" );

			}
		);

		writeOutput( future.get() );

	// Catch any errors triggered by the FutureNew().
	// --
	// NOTE: Hopefully, in the future, this won't be necessary!!!!!!!!
	} catch ( any error ) {

		writeDump( error );

	}

</cfscript>

Right off the bat, we have to wrap all of this in a Try/Catch because, unfortunately, Future errors in a proxy function don't work. Hopefully this is just a bug that will get fixed eventually, at which point the Try/Catch block can be replaced by an .error() function.

But, errors-aside, you can see that the FutureNew() function accepts a callback and returns a Future. The callback argument is invoked with a resolve() and a reject() method which must be used to drive the outcome of the generated Future. In this case, you'll see that I have a number of nested runAsync() methods in order to demonstrate that the resolve() and reject() methods are truly determining the outcome.

For the purposes of the demonstration, I have a series of resolve() and reject() calls in a row. Since I'm using an EmptyFuture to bridge the gap between these methods and the final Future, only the first resolve() or reject() invocation will matter. Subsequent calls will get silently ignored. This means that if we run the code as-is, we get the following ColdFusion output:

Resolution: So much win!

As you can see, our deeply-nested resolve() call successfully fulfilled our generated Future!

Now, if we comment-out the first resolve and re-run the page, we get the following ColdFusion output:

Resolution: Inner zen!

As you can see, this time we resolved using the result of a runAsync() function. The FutureNew() control-flow will look for Futures and try to resolve them before returning the result. As such, this intermediary Future gets implicitly unwrapped and its results get returned as the futureNew() outcome.

Now, if we comment-out the second resolve, the rest of the demos are rejections. And, since ColdFusion currently swallows all Future error details, the outcome of these fulfillments are meaningless. I mean, they do trigger the Try/Catch block in the calling context. But, there is no meaningful information provided in the Error objects. This is why the FutureNew() function writes the errors to the ColdFusion log before passing them down through the Future chain.

NOTE: Watch the video to see me run-through the all the conditions.

This was a frustrating but exciting experiment. I kept hitting issue after issue. But, I think I came up with half-way decent solutions for many of those issues. And, seeing this kind of, sort of work gives me hope that even more power can be unlocked with Futures if we can just get a bit more jiggy with it. If nothing else, I know I'm excited enough to continue digging further into Futures implementation in ColdFusion 2018.

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

Reader Comments

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.

15,902 Comments

@Chris,

Oh janky! Thanks for pointing that out -- sorry for not seeing your comment sooner. Let me see if I can figure out what the heck it was supposed to be.

15,902 Comments

@Vijay,

Sounds exciting -- do you have a sense of when the Update will be released? Or was it already released? I am having trouble finding a page that outlines when updates came out (like a Changelog somewhere).

27 Comments

@Ben,

The update containing the mentioned changes in Async Framework is not yet published. I will keep you posted on the schedule.

Thanks.
Vijay

247 Comments

Thanks @Ben. I've also wished we could pull JS's promise based api into CF. This is a great experiment. I'm impressed how far you were able to get.

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