Thursday, 16 July 2015

CFML: ColdFusion does some dodgy exception "handling"

G'day:
This was a curly one. I've been doing some testing recently, so I've been giving TestBox a bit of a thrashing. Yesterday I was a bit bemused that some of my tests were failing (that's not actually any sort of surprise given what I'm testing. Ahem), but other tests were erroring. yet they were testing much the same thing, and the condition causing the failure was the same for both the failing and erroring tests. I thought there was something weird afoot with TestBox, so I hit-up Luis about it.



I boiled the repro down to this:

// ErrorInsteadOfFail.cfc

component extends=testbox.system.basespec {

    function run(){
        describe("Replicating issue", function(){

            it("demonstrates a baseline", function(){
                expect(false).toBeTrue();
            });

            it("demonstrates the error", function(){
                var results = [1,2,3];
                results.each(function(result){
                    expect(false).toBeTrue();
                });
            });

        });
    }

}

This is really contrived, so don't pay too much attention to the code. Just focus on these two bits:

  • calling expect() directly in my test;
  • calling expect() within an iteration function, within an inline function expression.

The expectation on each is the same: that false is true. Which it isn't (I'm not going too fast, right? ;-), so the test should fail.

However I get this:


Notice how the second test isn't failing, it's actually erroring.

I raised a ticket for this: TESTBOX-127.

Luis came back to me a coupla hours later with the interesting observation that it seems when an exception is thrown within a closure, then ColdFusion doesn't just let it error, it wraps it up in a different exception, and throws that.

That's a bullshit thing for ColdFusion to be doing. Lucee, btw, does not do this: it behaves properly.

I expanded my tests out to cover a couple more things:



// ErrorInsteadOfFail.cfc

component extends=testbox.system.basespec {

    function run(){
        describe("Replicating issue", function(){

            it("demonstrates a baseline", function(){
                expect(false).toBeTrue();
            });

            it("demonstrates the error", function(){
                var results = [1,2,3];
                results.each(function(result){
                    expect(false).toBeTrue();
                });
            });
        });
        describe("Experimental variations", function(){
            describe("Using an iteration function", function(){

                it("checks if it's cos the function is inline", function(){
                    var results = [1,2,3];
                    var testFunctionViaExpression = function(){
                        expect(false).toBeTrue();
                    };
                    results.each(testFunctionViaExpression);
                });

                it("checks to see if it's just cos it's an iteration function (using a function statement)", function(){
                    var results = [1,2,3];
                    results.each(testFunctionViaStatement);
                });

                it("checks to see if it's just the method, or the procedural function does it too", function(){
                    var results = [1,2,3];
                    arrayEach(results, function(result){
                        expect(false).toBeTrue();
                    });
                });
            });

            describe("Just via a function", function(){

                it("checks to see if it's just cos it's a function expression", function(){
                    var testFunctionViaExpression = function(){
                        expect(false).toBeTrue();
                    };
                    testFunctionViaExpression();
                });

                it("checks to see if it's a problem with function statements", function(){
                    testFunctionViaStatement();
                });
            });

        });
    }

    function testFunctionViaStatement(){
        expect(false).toBeTrue();
    }

}

Here the new tests are fairly self-explanatory (on of the big benefits of TestBox's style of testing), but in summary:

  • I pre-define the function rather than have it inline;
  • I test with the procedural function, not the object method;
  • I test using the procedural version of the iteration method (arrayEach() rather than .each());
  • I test whether it's just throwing the exception in a function expression by itself (without the iteration function);
  • I test with a function defined via statement rather than expression.


The results for these are:
  • error
  • error
  • fail
  • fail
  • fail
So the problem only occurs when using the object method version of the iteration function, rather than the procedural function version; and whether or not the handler is defined via a function expression or a function statement.

Odd.

Here's a distillation of what's going on:

// exception.cfm
try {
    a = [1,2,3];
    a.each(function(){
        throw(type="InsideEachException", message="Blam", detail="An exception raised inside a closure function");
    });
} catch(any e){
    writeDump(e);
}

Here I'm throwing a specific exception: InsideEachException

But when we dump the exception out what do we see:


ColdFusion has ditched the original type, and is just throwing an unhelpful "Application" exception instead. note my original exception is still here, but buried in the "cause" property of the Application exception.

There's no way that ColdFusion should be hiding an exception like this. It's just not on. I'll raise a bug for this (4021994). In the mean time, just be a bit wary of this. It's quite a significant glitch, I think.

I hope to elicit an explanation of why they decided to do this too. That should be "informative".

Righto.

--
Adam