Select Page

Follow-Up On Error Handling During Async Iteration In ColdFusion

Ben Nadel
Published: January 6, 2024

I started to write this blog post, having completely forgotten that I already explored this exact topic back in 2020: A Closer Look At Error Handling During Parallel Array Iteration. In an ironic twist of fate, however, this turns out to be a helpful oversight because the error handling behavior has changed in Lucee CFML since that post; and, is now divergent from Adobe ColdFusion’s implementation. As such, even though it’s a bit redundant, I think it’s still worthy of a quick look at error handling during async iteration in ColdFusion – 2024 edition!

Parallel iteration, in ColdFusion, provides the ability to execute .each(), .map(), and .filter() on a collection using parallel threads. It is one of the many features that makes asynchronous work in ColdFusion relatively easy. And, for the most part, Adobe and Lucee have tried to strike a healthy balance between providing powerful tools and creating guard rails that prevent people from shooting themselves in the foot.

But, it’s not always clear where those guard rails exist; and, how they will manifest in the application. If we have ColdFusion code and it’s currently executing parallel iteration, what happens when one of the threads throws an error:

  • What happens to other parallel threads that are currently executing?

  • What happens to pending threads that have yet to be spawned?

  • What if two running threads each throw an error – where to those errors get surfaced?

I’m going to explore this in both Adobe ColdFusion 2023 and Lucee CFML 6, which are the latest versions of each CFML engine at the time of this writing (head nod to CommandBox for making this so easy to do). I’m going to iterate asynchronously over an array using N/2 threads; and, I’m going to throw an error in the first two threads.

Note: I have an Adobe ColdFusion (ACF) and Lucee CFML version of this code. In the ACF version, I mock out the dump() and systemOutput() functions. For the sake of brevity, I’m only going to show the Lucee version.

In the following code, note that I have a sleep(1000) at the top of each iteration. This blocks each thread long enough to ensure that ColdFusion has sufficient time to spawn the maximum number of parallel threads (N/2) before any error is thrown.

<cfscript>
	values = [ "a", "b", "c", "d", "e", "f", "g", "h" ];
	try {
		systemOutput( "#server.coldfusion.productName# - #server.lucee.version#", true );
		systemOutput( "Values: #values.len()#", true );
		// We're going to use parallel iteration to traverse the values array. And, we're
		// going to THROW AN ERROR in the first TWO value. We want to see how these errors
		// affect other threads already in process as well as values left to explore.
		values.each(
			( value, index ) => {
				systemOutput( "Entering thread (#index#)", true );
				sleep( 1000 );
				// Throw an error in first two threads.
				if ( index == 1 || index == 2 ) {
					systemOutput( "** BOOM (#index#) **", true );
					throw( type = "Boom.#index#" );
				}
				sleep( 1000 );
				systemOutput( "Exiting thread (#index#)", true );
			},
			true, // Parallel iteration.
			( values.len() / 2 ) // Parallel thread count.
		);
	} catch ( any error ) {
		dump( error );
	}
</cfscript>

If we run this Lucee CFML 6 code and watch the server logs, here’s what we see:

Terminal output for Lucee CFML 6 during asynchronous array iteration.

In this Lucee CFML 6 output, there are several important take-aways:

  • Any already-running parallel thread (in the asynchronous iteration) is allowed to complete even when another thread throws an error. As evidence of this, we can see that threads 3 and 4 exit successfully.

  • Any pending iterations for elements later in the array never start. As evidence of this, we can see that threads 7 and 8 never show up in the logs.

  • With the exception that any thread immediately following a crashed thread is allowed to start. As evidence of this, we can see that thread 5 is immediately spawned after the error in thread 1; and, we can see that thread 6 is immediately spawned after the error in thread 2. Note that this is before threads 3 and 4 have finished.

Ok, now let’s execute the same CFML in Adobe ColdFusion 2023:

Terminal output for Adobe ColdFusion 2023 during asynchronous array iteration.

In this Adobe ColdFusion 2023 output, we can see that all threads were allowed to execute even after the two errors were thrown.

This is what Lucee CFML 5.3 used to do, as per my post from 2020. As such, Lucee CFML 6 (or earlier) either has a regression. Or, it was an intentional change made at some point by the Lucee team. I’ll try to see if I can figure that out and leave a comment.

This wasn’t illustrated in the terminal output; but, in both cases (Adobe ColFusion and Lucee CFML), only the first thrown error, BOOM.1, is surfaced in the top-level page (via the try/catch). The second error, BOOM.2, fails silently in the background (and is presumably logged to an error log somewhere).

To be honest, both behaviors make sense to me. On the one hand, I can see why you’d want the entire collection to be iterated before an error is surfaced. But, on the other hand, I can totally understand wanting to short-circuit the iteration when an error is encountered. As such, neither approach is obviously right or wrong in my eyes.

The lesson learned here is that you should probably test your current CFML runtime to see how it behaves during asynchronous iteration. And, perhaps even better, build error handling into your async iteration so that both the happy paths and sad paths are accounted for.

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

Source: www.bennadel.com