DevLearningTools

MODULE 12 · LESSON 04

Throwing Exceptions

Deliberately raising an exception with throw/cfthrow, custom exception types and their dotted hierarchy, attaching structured data with extendedInfo, and Lucee's cause parameter for exception chaining.

New lessons are added one at a time as the course gets built out — a graded quiz for each lesson is still on the way.

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

Tag Syntax
<cfif empQuery.recordCount LT 1>
    <cfthrow type="NoQueryResult" message="No matching employee was found.">
</cfif>
CFScript
if (empQuery.recordCount < 1) {
    throw(type = "NoQueryResult", message = "No matching employee was found.");
}

The Full Attribute Reference

AttributeMeaning
messageA description of what went wrong
typeA custom type string (defaults to "Application"); can use dots to build a hierarchy
detailA more detailed description; the interpreter appends the error's position if it goes uncaught
errorCodeA custom error code the application defines itself
extendedInfoCustom structured data, commonly a JSON string, for the catch block to read back
objectThrows 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

CFScript
// 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)
    );
}
CFScript — reading it back
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

CFScript
throw(type = "Payment.CardDeclined", message = "The card was declined.");
// or elsewhere:
throw(type = "Payment.InsufficientFunds", message = "Insufficient funds.");
NOTE

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+)

CFScript
try {
    processPaymentBatch(batch);
} catch (any e) {
    throw(message = "Payment batch processing failed", cause = e);
}
NOTE

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.