Skip to main content
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Jacob Munson
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Jacob Munson

Functions For Coalescing Values In ColdFusion

By
Published in Comments (2)

In BigSexyPoems, I've been maintaining a CFWheels-inspired file of globally available ColdFusion user defined functions (UDFs). Last week, I added several UDFs in order to be able to "coalesce" a set of inputs down into a single value. Specifically, I wanted to be able to create a default poem name if the user didn't provide one.

Consider a form submission workflow that includes a form.name input. In order to remove friction in this particular workflow, I'm going to programmatically override the empty-string as an input:

form.name = coalesceTruthy( form.name, "My New Poem" )

Adobe ColdFusion already has two operators that somewhat address this problem space:

  • Elvis Operator (?:)
  • Null-Coalescing Operator (??)

Pre-ACF-2025, I actually could have used the Elvis operator (?:) to solve this problem. This is because there was a bug in the ACF implementation that treated empty-strings as "null" values. Ironically, they've fixed that bug in ACF 2025 which no longer makes it relevant for my use-case.

And of course, the null-coalescing operator (??) doesn't make sense either because the empty-string isn't null and therefore won't be skipped-over during the form processing.

Aside: The Elvis operator is awesome, but is inconsistent across versions of Adobe ColdFusion (see above) and is critically different than the implementation in Lucee CFML (which, frankly, got it right). As such, I only use the Elvis operator when I fully understand the problem space of the inputs being consumed.

Giving these issues, I've solved my problem by creating a user defined function named, coalesceTruthy(). This function returns the first "Truthy" argument. "Truthy", in this case, is using the JavaScript standard, which says that all values which aren't explicitly "Falsy" are "Truthy". To this end, I've created two UDFs for differentiating truthy / falsy values:

<cfscript>

	/**
	* I determine if the given value is a Falsy (according to JavaScript rules).
	*/
	private boolean function isFalsy( any value ) {

		if ( isNull( value ) ) {

			return true;

		}

		if ( ! isSimpleValue( value ) ) {

			return false;

		}

		if ( isString( value ) ) {

			return ! value.len();

		}

		if ( isBoolean( value ) || isNumeric( value ) ) {

			return ! value;

		}

		return false;

	}


	/**
	* I determine if the given value is a Truthy (according to JavaScript rules).
	*/
	private boolean function isTruthy( any value ) {

		if ( isNull( value ) ) {

			return false;

		}

		return ! isFalsy( value );

	}

</cfscript>

As you can see, any value that isn't Falsy is implicitly Truthy.

Another complexity of ColdFusion, vis-a-vis truthy coalescing, is the fact that ColdFusion can represent non-string values as strings. Specifically, Booleans, numbers, and date can all seamlessly cast between string and non-string contexts. Which means that ColdFusion will happily report the string "false" as a Boolean; and, willy happily consider it a "falsy".

While this flexibility is part of what makes ColdFusion so easy to work with, it does come with a cost. And, in this case, the cost is that I have to manually differentiate between actual string values and string-like values. If you look back up the isFalsy() decision function, you'll see that I use a UDF, isString(), after I've determined I'm working with a simple value. In this case, all this UDF does is check to see that I'm working with a Java string instance:

<cfscript>

	/**
	* I determine if the given value is a native string.
	*/
	private boolean function isString( required any value ) {

		return isInstanceOf( value, "java.lang.String" );

	}

</cfscript>

Now that we have our truthy / falsy concepts down, we have to deal with the mechanics of variadic functions. In my specific use case, I'm only passing-in two inputs. But, I'd like the coalesceTruthy() function to be able to accept N-arguments.

For this, I need a way to convert an arguments scope into a unified array. This is done with another user defined function, argumentsToArray():

<cfscript>

	/**
	* I convert the given arguments collection into a proper array.
	*/
	private array function argumentsToArray(
		required any args,
		boolean includeNullValues = true
		) {

		var result = arrayMap( args, ( arg ) => arguments?.arg );

		if ( includeNullValues ) {

			return result;

		}

		return result.filter( ( value ) => ! isNull( value ) );

	}

</cfscript>

The arrayMap() function seems to be the best for consistently creating arrays (from arguments) across the various CFML platforms. If you're curious to see the set of bugs and issues, look at the previous link.

Now that we have our truthy / falsy values sorted, and our strings differentiated from our non-strings, and the ability to consume N-arguments, we can finally get to the business of coalescing inputs. For this, I have two functions:

  • coalesce() - which returns the first non-Null value.
  • coalesceTruthy() - which returns the first truthy value.

You'll see that these two very simple functions build on top of all the previously mentioned functions:

<cfscript>

	/**
	* I return the first non-null argument.
	*/
	private any function coalesce(/* variadic function */) {

		// Caution: the `false` argument omits null values during conversion.
		for ( var value in argumentsToArray( arguments, false ) ) {

			return value;

		}

	}


	/**
	* I return the first truthy / non-falsy argument.
	*/
	private any function coalesceTruthy(/* variadic function */) {

		// Caution: the `false` argument omits null values during conversion.
		for ( var value in argumentsToArray( arguments, false ) ) {

			if ( isTruthy( value ) ) {

				return value;

			}

		}

	}

</cfscript>

Both of these functions will return "undefined" if no input meets the necessary criteria. It's up to the developer to always define an appropriate fall-through value as the last argument, when needed.

Let's take a quick look at these two functions in action:

<cfscript>

	// Pull-in the user defined functions.
	include "/core/cfmlx.cfm";

	dump([
		[
			test: "coalesce( nullValue(), false, '', 'dogo' )",
			result: coalesce( nullValue(), false, '', 'dogo' )
		],
		[
			test: "coalesceTruthy( nullValue(), false, '', 'dogo' )",
			result: coalesceTruthy( nullValue(), false, '', 'dogo' )
		],
	]);

</cfscript>

When we run this Adobe ColdFusion 2025 code, we get the following output:

  • Test: coalesce( nullValue(), false, '', 'dogo' )
  • Result: false
  • Test: coalesceTruthy( nullValue(), false, '', 'dogo' )
  • Result: dogo

As you can see, the coalesce() function skipped over the nullValue() and used false as the first non-null input. coalesceTruthy(), on the other hand, skipped over nullValue(), false, and '' (empty string), and used dogo as the first truthy.

On its face, coalescing values seems like a simple concept. But, in ColdFusion — with many of its historical web-facing choices — we had to iteratively build-up to it. To paraphrase Kent Beck, we made the change easy (all of the supporting functions); and then, we made the easy change.

Epilogue On the Composition of cfmlx.cfm

My BigSexyPoems ColdFusion app includes the cfmlx.cfm (CFML e(X)tensions) template into every execution context. This is a vanilla file and doesn't use application-level constructs like dependency-injection (DI) and privately-scoped data. It's intended to be an extension to platform that knows nothing about the application domain. As such, I limit the use of global UDFs to be pure functions that deal primarily with data-structure manipulation, data-type decisions, convenience aliases, and polyfills (ie, re-implementing the stuff that Lucee CFML got right).

I should be able to copy-paste this file into any Adobe ColdFusion application and have it "just work" (naming conflicts across versions not withstanding).

I'm loving it; but, to be honest, it does make me a little nervous. I've spent so many years working on good encapsulation strategies; and now, I'm kind of blowing that up. At least for a small slice of functionality. That said, until it actually proves to be a problem, I'm trying to let the pragmatic side of my brain win-out over the academic side of my brain.

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

Reader Comments

293 Comments

This is all super interesting to step through. I'm now nervous about having used Elvis operatiors throughout my applications. Thanks for that 😜

I'm having trouble wrapping my head around why this blows up any encapsulation strategies you have. Would you mind giving us more words about that?

Personally, I've always created utility functions, preferring them to be in the global scope of my app. The biggest concern I have is remembering to use them. In the context of a multi developer environment, that can be especially challenging because I may not be aware of utility functions other devs created (and vice versa) and varying naming conventions (and poor document) make discovery challenging

16,109 Comments

@Chris,

Ha ha, you're welcome 😛 really, the only truly problematic edge-case that I've ever run into is that ACF treats the [empty string] as a "falsy" value in pre-2025, and Lucee CFML did not. Which means that if you took code like this:

( value ?: fallback )

... that was written and proven to be working in Lucee CFML and then copied it into ACF (pre-2025), you'd be introducing slightly different behavior. So, it's more about copy across platforms, not about having code within your application.

When I said it blows up the encapsulation, I only meant that historically I've tried to wrap everything up in CFCs (including collections of utility functions) that I would then DI into other contexts. So, basically, I've replaced this:

property name="utils" type="lib.utils.Utilities";

... with this:

include "./utils.cfm";

The former approach (CFC) can have things like privately scoped data and other objects injected into it. The latter approach (CFM) shares the same scope as the calling context, so collisions and scope pollution have to be taken more seriously.

It's all just trade-offs - isn't everything?

But yeah, remembering I have helpful functions in there is half the battle.

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
Managed hosting services provided by:
xByte Cloud Logo