This kinda got out of control. I was meaning to be writing an article about PHP7's improvements to PHP's error handling, but whilst writing some comparative CFML code I got bogged-down looking at difference in behaviour between different types of throwables, and how ColdFusion and Lucee handle them differently. I ended up with a fair bit of code, and not much of it would be needed for the PHP article, so I've decided to write it up separately.
Be warned, I've seen ditch water less dull than this lot.
This investigation stemmed from my continuing horror at how rubbish PHP is at error handling, in comparison to CFML. Basically in CFML there seems to be three error situations:
Runtime errors
If code can compile, but then when it is run some sort of error arises: it's bubbled back to the application as some manner of exception. And exceptions can be caught and error handled. Or they can be left, and the globalonError()
handler will deal with them.Parsing errors
If a file can't even parse, it can't be compiled, so it won't execute and an exception is thrown. One cannot try/catch parsing errors within the same file they occur, because try/catching occurs and run time, and if the file can't compile, it can't run. So in the context of that file, there's not "run time". However the code that calls the file that can't compile still receives an exception it can error handle.JVM errors
It's possible to flatten the JVM with sufficiently shonky code or some "unexpected" behaviour. For example simply consuming all the JVM's memory faster than it can garbage collect it. In this situation the JVM will either go into a loop or if it's still in control of things, pull the plug on the running request. This is not try/catchable, for obvious reasons.My explanation of this last one might be vague / inaccurate... I have no idea how the JVM deals with this stuff, but I do know it's possible to kill a CF instance's JVM with bad / "unlucky" code.
I'd never really given much thought to all this before. My understanding of errors in CFML is basically:
- crap code will not parse. You can't expect to error-handle that. Fix the code.
- everything else is an exception, and one can deal with it.
Whilst prepping for the PHP-centric article I was reminded of something I saw on Stack Overflow last week (or so): "ColdFusion not catching NoClassDefFoundError". Leigh is one of the top CFML devs out there, and I always learn stuff when he has something to say. His answer here is no exception (unfortunate pun there).
Java has a hierarchy of error-ish sort of things:
java.lang.Object
java.lang.Throwable
java.lang.Error
java.lang.Exception
And what Leigh has put me onto is that ColdFusion will only actually catch java.lang.Exceptions. It won't catch Errors, and it won't catch Throwables (unless they happen to be Exceptions). Interesting. Kinda.
Java.lang.Error
OK, so what's the meaningful ramification of all this. Here's some test code:// javaLangError.cfm
runsafe(function(){
writeOutput("Force an exception with code<br>");
var result = 1/0;
});
runsafe(function(){
writeOutput("Create an Exception and throw it<br>");
javaLangException = createObject("java", "java.lang.Exception").init("Exception Message");
throw(type=javaLangException);
});
runsafe(function(){
writeOutput("Create an Error and throw it<br>");
javaLangError = createObject("java", "java.lang.Error").init("Error Message");
throw(type=javaLangError);
});
runsafe(function(){
writeOutput("Call a java method which throws an Error<br>");
writeOutput("Before java.lang.Error is thrown<br>");
createObject("java", "me.adamcameron.miscellany.TestErrors").throwsError();
writeOutput("After java.lang.Error is thrown<br>");
});
runsafe(function(){
writeOutput("After everything");
});
function runsafe(task){
try {
task();
}catch (any e){
writeOutput("
type: [#e.type#]<br>
message: [#e.message#]<br>
detail: [#e.detail#]<br>
");
}finally {
writeOutput("<hr>");
}
}
Here I perform four tests:
- Just have some code that will cause an exception: 1/0 dun't work;
- create a java.lang.Exception object, then throw it from CFML;
- create a java.lang.Error object, then throw it from CFML;
- call some Java code which - internally - throws a java.lang.Error
Force an exception with code
type: [Expression]
message: [Division by zero.]
detail: [Division by zero is not allowed.]
Create an Exception and throw it
type: [java.lang.Exception: Exception Message]
message: []
detail: []
Create an Error and throw it
type: [java.lang.Error: Error Message]
message: []
detail: []
Call a java method which throws an Error
Before java.lang.Error is thrown
What we see here is Exceptions are handled uniformly. Good.
However it gets weird with Errors. If I create an Error and then throw it from CFML, it's catchable. If, however, the Error is thrown from Java, then ColdFusion... well... just dies. Note that I don't get the error details output by the "Call a java method which throws an Error" test, and indeed I don't get the "After everything" message. Oddly, I do get the
<hr>
output by the finally block of that test.In the logs I get this:
application.log:
"Error","http-bio-8511-exec-1","06/14/15","16:57:52",,"Error Message The specific sequence of files included or processed is: C:\blogExamples\cfml\exceptions\javaLangError.cfm, line: 25 "
exception.log:
"Error","http-bio-8511-exec-1","06/14/15","16:57:52",,"Error Message The specific sequence of files included or processed is: C:\blogExamples\cfml\exceptions\javaLangError.cfm, line: 25 "
java.lang.Error: Error Message
at me.adamcameron.miscellany.TestErrors.throwsError(TestErrors.java:6)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at coldfusion.runtime.java.JavaProxy.invoke(JavaProxy.java:97)
at coldfusion.runtime.CfJspPage._invoke(CfJspPage.java:2483)
at cfjavaLangError2ecfm302279409$func_CF_ANONYMOUSCLOSURE_3.runFunction(C:\blogExamples\cfml\exceptions\javaLangError.cfm:25)
(and obviously that exception goes on and on from there. You get the point thought).So Leigh's right. ColdFusion doesn't handle java.lang.Errors at all well.
Oh, btw, the Java code I was running for this is:
package me.adamcameron.miscellany;
public class TestErrors {
public static void throwsError() {
throw new java.lang.Error("Error Message");
}
}
java.lang.Throwable
OK, so ColdFusion doesn't deal with Errors at all well. What about just Throwables (ie: java.lang.Throwable)?I created a wee class that extends java.lang.Throwable (admission: it never occurred to me that java.lang.Throwable would be a concrete class: I assumed it was abstract, so rolled my own. I could have tested this with a vanilla Throwable object, I guess):
package me.adamcameron.miscellany;
public class ThrowableNonException extends java.lang.Throwable {
public ThrowableNonException(String message){
super(message);
}
}
And I added this to the TestErrors class:
public static void throwsThrowableNonException() throws ThrowableNonException {
throw new ThrowableNonException("ThrowableNonException Message");
}
And... tested this via ColdFusion:
// javaLangThrowable.cfm
// [...]
runsafe(function(){
writeOutput("Before me.adamcameron.miscellany.ThrowableNonException is thrown<br>");
createObject("java", "me.adamcameron.miscellany.TestErrors").throwsThrowableNonException();
writeOutput("After me.adamcameron.miscellany.ThrowableNonException is thrown<br>");
});
// [...]
And the output is:
Before me.adamcameron.miscellany.ThrowableNonException is thrown
And, once again, in the logs:
Application.log:
"Error","http-bio-8511-exec-3","06/15/15","13:18:39",,"ThrowableNonException Message The specific sequence of files included or processed is: C:\blogExamples\cfml\exceptions\javaLangThrowable.cfm, line: 16 "
Exception.log:
"Error","http-bio-8511-exec-3","06/15/15","13:18:39",,"ThrowableNonException Message The specific sequence of files included or processed is: C:\blogExamples\cfml\exceptions\javaLangThrowable.cfm, line: 16 "
me.adamcameron.miscellany.ThrowableNonException: ThrowableNonException Message
at me.adamcameron.miscellany.TestErrors.throwsThrowableNonException(TestErrors.java:10)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at coldfusion.runtime.java.JavaProxy.invoke(JavaProxy.java:97)
at coldfusion.runtime.CfJspPage._invoke(CfJspPage.java:2483)
at cfjavaLangThrowable2ecfm1195292406$func_CF_ANONYMOUSCLOSURE_0.runFunction(C:\blogExamples\cfml\exceptions\javaLangThrowable.cfm:16)
So ColdFusion will not catch a throwable either. It can only catch exceptions.For further good measure I just verified whether Java can catch Errors and Throwables, but adding these methods into my TestErrors class:
public static String catchesError() {
try {
throw new java.lang.Error("Error Message");
} catch (java.lang.Error e){
return "Caught: " + e.getMessage();
}
}
public static String catchesThrowable() {
try {
throw new ThrowableNonException("ThrowableNonException Message");
} catch (ThrowableNonException e){
return "Caught: " + e.getMessage();
}
}
And then called them with this sort of rig (this is the one for Errors, the Throwable one is the same, except calling the appropriate method):
// catchErrorInJava.cfm
writeOutput("Before java.lang.Error is caught<br>");
result = createObject("java", "me.adamcameron.miscellany.TestErrors").catchesError();
writeOutput("result: #result#<br>");
writeOutput("After java.lang.Error is caught<br>");
This outputs:
Before java.lang.Error is caught
result: Caught: Error Message
After java.lang.Error is caught
So Java can catch these things. Unsurprising, really.
Lucee
Right, so what about Lucee? Running the same code, I get these results:Force an exception with code
type: [java.lang.ArithmeticException]
message: [Division by zero is not possible]
detail: []
Create an Exception and throw it
type: [java.lang.Exception: Exception Message]
message: []
detail: []
Create an Error and throw it
type: [java.lang.Error: Error Message]
message: []
detail: []
Call a java method which throws an Error
Before java.lang.Error is thrown
type: [java.lang.Error]
message: [Error Message]
detail: []
After everything
It deals with everything just fine. What about the Throwable?
Before me.adamcameron.miscellany.ThrowableNonException is caught
result: Caught: ThrowableNonException Message
After me.adamcameron.miscellany.ThrowableNonException is caught
This is more like what I'd expect.
I've now bored the trews off meself, so I dunno how you are feeling about all this. But well done: you got to the bottom of it. Now I can start working on the code for the article I meant to write this weekend!
Bug raised: 4010446.
Righto.
--
Adam