Not every failure is something CFML itself detects as an error, a business rule being violated, an invalid state that only the application logic knows about. throw (cfthrow in tag syntax) lets code raise an exception deliberately, on its own terms, so it can be caught and handled the same way any other exception would be.
Learning Objectives
After completing this lesson, you'll be able to:
- Throw a custom exception with a specific type, message, and detail.
- Attach structured data to a thrown exception with extendedInfo, and read it back out in the catch block.
- Chain a new exception to the one that caused it, using Lucee's cause attribute.
How throw Fits In
Business Rule Violated
throw / cfthrow
Propagates Upward
Caught by a Matching catch
A Basic Custom Exception
<cfif empQuery.recordCount LT 1>
<cfthrow type="NoQueryResult" message="No matching employee was found.">
</cfif>if (empQuery.recordCount < 1) {
throw(type = "NoQueryResult", message = "No matching employee was found.");
}The Full Attribute Reference
| Attribute | Meaning |
|---|---|
| message | A description of what went wrong |
| type | A custom type string (defaults to "Application"); can use dots to build a hierarchy |
| detail | A more detailed description; the interpreter appends the error's position if it goes uncaught |
| errorCode | A custom error code the application defines itself |
| extendedInfo | Custom structured data, commonly a JSON string, for the catch block to read back |
| object | Throws an existing Java exception object directly, mutually exclusive with every other attribute |
| cause (Lucee 6+) | Attaches an original exception as the new one's root cause, for exception chaining |
A Real Example: Structured Validation Errors
extendedInfo is just a string, CFML doesn't structure it for you. Serializing a real data structure into JSON before throwing, and deserializing it back out in the catch block, is a genuinely common pattern for passing detailed, structured error data across the boundary.
Validate Input
throw, extendedInfo = JSON
catch, deserializeJSON(extendedInfo)
Structured API Response
// Throwing with structured data
if (len(trim(form.email)) == 0 || len(trim(form.password)) == 0) {
fieldErrors = {};
if (len(trim(form.email)) == 0) fieldErrors.email = "Email is required.";
if (len(trim(form.password)) == 0) fieldErrors.password = "Password is required.";
throw(
type = "Validation",
message = "Registration validation failed.",
extendedInfo = serializeJSON(fieldErrors)
);
}try {
registerUser(form);
} catch (Validation e) {
fieldErrors = deserializeJSON(e.extendedInfo);
// fieldErrors.email, fieldErrors.password, etc. — ready for a JSON API response
}Custom Type Hierarchies in Practice
throw(type = "Payment.CardDeclined", message = "The card was declined."); // or elsewhere: throw(type = "Payment.InsufficientFunds", message = "Insufficient funds.");
A catch block listening for the broader "Payment" type catches both of these, and any other type nested under it, without needing to list each specific case individually.
Chaining an Exception to Its Root Cause (Lucee 6+)
try {
processPaymentBatch(batch);
} catch (any e) {
throw(message = "Payment batch processing failed", cause = e);
}The original exception e is preserved as the new one's cause, showing up as "Caused by" in the stack trace, rather than being lost when it's wrapped in a more general, higher-level exception.
Common Beginner Mistakes
Assuming extendedInfo automatically structures data
It's just a plain string. Serializing a real structure to JSON before throwing, and deserializing it back out in the catch block, is a manual but standard pattern.
Combining object with other attributes
object throws an existing Java exception directly and is mutually exclusive with every other cfthrow attribute, they can't be combined in the same throw.
Assuming type defaults to something generic like "any"
It defaults to "Application", not any, when no type is specified.
Losing the original exception when wrapping it in a new, more general one
On Lucee 6+, passing the original as cause preserves it in the stack trace as the root cause, rather than discarding its detail entirely.
Best Practices
- Use a specific, meaningful custom type, and consider a dotted hierarchy for a family of related exceptions.
- Serialize structured data to extendedInfo when a catch block genuinely needs more than a plain message, rather than cramming everything into the message string.
- Chain the original exception with cause (where available) instead of silently discarding it when wrapping it in something more general.
- Reserve object for the specific case of rethrowing an actual Java exception, not as a general-purpose way to throw.
Interview Questions
What does type default to on a thrown exception if none is specified?
"Application", not any or a blank value.
How would you pass structured, per-field validation errors through a thrown exception?
Serialize the structure to JSON and pass it as extendedInfo, then deserialize it back out in the catching code with deserializeJSON.
Why is object mutually exclusive with every other cfthrow attribute?
It throws an existing Java exception object directly, at that point there's nothing left for message, type, or the other attributes to configure separately.
What does Lucee's cause attribute on throw do?
It attaches an existing exception as the new one's root cause, preserving it in the stack trace as "Caused by" instead of losing that detail when the exception is wrapped in a more general one.
Summary
In this lesson, you threw custom exceptions with a specific type and message, attached structured data with extendedInfo and read it back out in a catch block, used a dotted type hierarchy for a family of related exceptions, and chained an exception to its root cause with Lucee's cause attribute.
What's Next?
The next lesson covers onError in Application.cfc, the application-wide catch-all for any exception that escapes every page-level try/catch.