Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Shvejan Shashank
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Shvejan Shashank

Safe-Navigation Operator Swallows Method Errors In Adobe ColdFusion 2023

By
Published in Comments (6)

This morning, I was running into a strange null-reference error in my ColdFusion dependency injector (DI). A CFProperty-based component wasn't being injected; but, no error was being thrown or logged. After commenting-out a bunch of code, I finally narrowed it down to a bug in ColdFusion. If you use the safe-navigation operator to invoke a component method, any error thrown in that method will be swallowed up and the method will be short-circuited. I confirmed this behavior in both Adobe ColdFusion (ACF) 2021 and 2023.

To see this in action, let's create a ColdFusion component that invokes three methods. The second method in the procedure won't exist and should throw an error:

component {

	public void function setData() {

		this.data = {};
		this.data.append({ key1: "value1" });
		this.data.appenddddddddd({ key2: "value2" }); // <--- ERROR.
		this.data.append({ key3: "value3" });

	}

}

As you can see, the second method call to .appenddddddddd() is nonsense and should throw an error. In our first test, we're going to invoke the .setData() method using the safe-navigation operator:

<cfscript>

	thing = new Thing();

	thing?.setData(); // <--- this line SHOULD error.

	writeDump( thing );

</cfscript>

If we run this code in either ACF 2021 or 2023, we get this page output:

Screenshot of a CFDump showing an instance of Thing.cfc that only has one public key, key1.

As you can see, no error was observed in the top-level script—the code appeared to run correctly. However, if we examine the public keys on the Thing.cfc instance, we can see that only key1 was set. The call to set key2 errored-out and the .setData() method was short-circuited. But, no error was raised in the calling context (or logged to the console).

If we try to run the same code without the safe-navigation operator, we get the expected experience:

<cfscript>

	thing = new Thing();

	thing.setData();

	writeDump( thing );

</cfscript>

As you can see, this is the same code except that we're executing .setData() instead of ?.setData(). And, when we run this ColdFusion code, we get the expected error:

Screenshot of a ColdFusion error saying: The appenddddddddd method was not found.

This is the expected behavior. This is what the safe-navigation operator should be doing as well. To get around this, I can of course rewrite the safe-navigation operator to use a structKeyExists() condition:

<cfscript>

	thing = new Thing();

	if ( structKeyExists( thing, "setData" ) ) {

		thing.setData(); // <--- this line SHOULD error.

	}

	writeDump( thing );

</cfscript>

But, this is just a work-around. I will file a bug in the Adobe Bug Tracker and link to it in the comments.

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

Reader Comments

238 Comments

This may be a dumb question, so feel free to roast me. But why wouldn't the method be there? I'm assuming you know the component and that the component's interface has the particular method, so why test for it at all if you know it exists? 😬

15,849 Comments

@Chris,

Great question! There are a couple of reasons why a call like this might exist. In my case—with my Injector.cfc—I'm explicitly calling the .init() and .$init() methods. When I'm constructing a ColdFusion component manually with a createObject() call, I have to call the .init() method on it afterward. However, I'm not sure if the given component has it (though, it's possible that ColdFusion would paper-over that one, I'm not sure).

Then, after I instantiate and wire-in all the dependencies, I call an optional "after init" method called .$init(). Basically you can put your pre-injection logic in .init() and your post-injection logic in .$init(). Both of which are technically optional.

This isn't relevant to my particular use-case; but, the ?. operator actually checks both sides of the operation. Meaning, when you call:

a?.b()

ColdFusion is first checking to see if a exists; and, if it does, it then checks to see if b exists before executing the overall expression. This is super helpful when it comes to cleaning-up transient resources. For example, you might have a try/finally block to clean-up a Redis connection with a call like:

try {
	var resource = getResource();
	// .... use resource ....
} finally {
	resource?.close();
}

In this case, the finally block will only call the .close() method if the resource variable exists. So, in this case, it's not the method that we're worried about, it's the host variable. Of course, it doesn't matter what our intentions were - if the .close() method in this case were to throw an error, it seems that ColdFusion would just swallow it up (the bug).

15,849 Comments

Also, David Patricola on LinkedIn just pointed out that I had a thing.$init() reference in my code in the post, but it should have been thing.setData(). I've updated the blog post.

238 Comments

@Ben Nadel,

Thanks for the explanation, that makes sense. Unfortunately, now your explanatory comment references thing.init() and thing.$init() which the article no longer references. Fortunately, it all still makes sense. Appreciate your time, your patience, and most of all...your generosity in calling it a "Great question" haha

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