Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Justine Arreche
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Justine Arreche ( @saltinejustine )

Follow-Up On Error Handling During Async Iteration In ColdFusion

By on
Tags:

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.

Reader Comments

426 Comments

Hi Ben
Interesting topic.
Normally, I would expect subsequent iterations, after an error, to work, only if the cftry block is inside the loop? If the cftry is outside the loop, then I would expect the loop to stop running, once it encounters the first error.
Well, I mean this is what happens when you use something like cfloop?
But, I must say, I'm not really that familiar with the each construct, and I never realised that it spawned a thread per iteration? But, maybe that is a consequence of the async=true param?

15,688 Comments

@Charles,

Yes, exactly, it's the optional arguments at the end:

.each(
	callback,
	parallel = false, // Default.
	threadCount = 20 // Default.
)

I'm with you, though - the ideal scenario is to have a try/catch inside the callback.

And sorry, I'm still working on fixing the mobile-comment form. My Mac OS is apparently too old to allow for remote debugging when I plug-in my iPhone 😟 I had added Hotwire Turbo to my site last year. I think I might just rip it out and go back to handling control-flow with vanilla JavaScript.

426 Comments

Hi Ben

Thanks for the response.

I know this is a little bit cheeky, because this refers to a "Oh my chickens, this post is old!" post, but I wanted to ask you something about CGI.HTTP_REFFERRER:

www.bennadel.com/blog/903-passing-referer-as-coldfusion-cfhttp-cgi-value-vs-header-value.htm

I am using FWI, which has a neat little setting for CORS, in the application.cfc file:

optionsAccessControl = {
		  origin: local.origin,
		  headers: local.headers,
		  credentials: true
}

Now, as we know there is no reliable way of adding multiple URLS, as a value for:

Access-Control-Allow-Origin

So, I thought I would do a neat little thing, I learned from a PHP SO post:

https://stackoverflow.com/questions/1653308/access-control-allow-origin-multiple-origin-domains

I am going to do something like:

local.origin = "http//www.foo.com, http//www.bar.com"; // actually this value comes from my global settings DB table

if(ListFindNoCase(local.origin, CGI.HTTP_REFFERRER)){
    local.origin = CGI.HTTP_REFFERRER;
}

optionsAccessControl = {
		  origin: local.origin,
		  headers: local.headers,
		  credentials: true
}

Now, why am I asking you this?
Well, am I right in saying that it would be impossible to spoof the CGI.HTTP_REFFERRER, if it is coming from an XHR request?
I understand that it can be spoofed easily, if it comes from a non browser origin, like a cURL or CFHTTP request, but I think because of CORS, it cannot be change, if it is sent from a browser client?
And secondly, is CGI.HTTP_REFFERRER, reliable?
Please remember, it will be sent from a domain that I own & administer, to a domain that I own & administer, so I am hoping it will always be sent, otherwise, I cannot use this paradigm.

15,688 Comments

@Charles,

So, to the best of my understanding, you are correct. 1) the http_referer value can be spoofed from server-side scripts (it's just an HTTP header). And 2) the http_referer value won't be spoofed if the request is coming from a "normal browser". I quote "normal browser" because I do believe you can get security plug-ins and other mechanics that will modify the outbound request (I think most commonly people will strip-out the referrer so that you don't know where people are coming from). But, I don't believe there's any way to trick a "normal user" into doing something malicious with http_referer.

15,688 Comments

@Charles,

As a quick follow-up, I just looked at our production code to see how we're handling the CORS stuff, and it's almost exactly what you have as well (only we didn't know that FW/1 has something built-in for it, so we're just using CFHeader to set the value). But yeah, it looks like this:

var corsAllowList = [ ... ];
var origin = getHeader( "origin" );

if ( arrayContains( corsAllowList, origin ) ) {
	setHeader( "Access-Control-Allow-Origin", origin );
	setHeader( "Access-Control-Allow-Credentials", true );
}

So yeah, basically what you have.

Post A Comment — I'd Love To Hear From You!

Post a Comment

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