A custom exception in Apex is a class that extends the built-in Exception type. You throw it when your code hits a domain-specific failure that the platform’s built-in exceptions don’t describe well — a business rule violation, an integration mapping failure, a state transition that shouldn’t happen. The convention is one custom exception per failure mode, with a name that ends in Exception.
Minimal custom exception
public class OrderValidationException extends Exception {}
That’s the whole class. Apex’s Exception base class already gives you the constructors and methods you need — getMessage(), getStackTraceString(), getCause(), and the constructor that takes a String message.
Throwing and catching
public class OrderProcessor {
public static void process(Order__c order) {
if (order.Status__c == 'Closed') {
throw new OrderValidationException(
'Cannot process order ' + order.Name + ' — already closed'
);
}
if (order.Total__c == null || order.Total__c <= 0) {
throw new OrderValidationException(
'Order ' + order.Name + ' has invalid total: ' + order.Total__c
);
}
// ... happy path ...
}
}
// Caller
try {
OrderProcessor.process(myOrder);
} catch (OrderValidationException e) {
// Caller specifically wanted to handle bad orders
System.debug('Validation failed: ' + e.getMessage());
Logger.warn(e);
} catch (DmlException e) {
// Caller also wants to handle DML failures
Logger.error(e);
} catch (Exception e) {
// Catch-all for anything else
Logger.error(e);
throw e; // re-throw so the transaction rolls back
}
A richer custom exception with extra context
You can add fields and constructors to carry structured context that callers might want to inspect:
public class IntegrationException extends Exception {
public Integer httpStatus;
public String endpoint;
public String responseBody;
// Convenience constructor that takes structured data
public IntegrationException(
String message,
Integer httpStatus,
String endpoint,
String responseBody
) {
this(message); // call the inherited Exception(String) ctor
this.httpStatus = httpStatus;
this.endpoint = endpoint;
this.responseBody = responseBody;
}
}
// Usage
HttpResponse res = http.send(req);
if (res.getStatusCode() >= 400) {
throw new IntegrationException(
'Stripe API returned an error',
res.getStatusCode(),
req.getEndpoint(),
res.getBody()
);
}
// Caller can branch on the structured fields
try {
StripeClient.charge(order);
} catch (IntegrationException e) {
if (e.httpStatus == 429) {
// rate limited — retry later
System.enqueueJob(new RetryStripe(order.Id));
} else if (e.httpStatus >= 500) {
// upstream error — log and alert
Logger.error(e);
} else {
// client error — fail loudly
throw e;
}
}
Exception hierarchy
You can build a small hierarchy of custom exceptions so callers can catch broad or specific types:
public virtual class AppException extends Exception {}
public class OrderValidationException extends AppException {}
public class PaymentException extends AppException {}
public class IntegrationException extends AppException {}
Now any caller that wants to handle all app-level failures uniformly can catch (AppException e), while callers that care about one specific type can catch the narrower class.
What the platform requires
Apex enforces three rules on custom exception classes:
- Must extend
Exception(directly or through another custom exception). - Class name must end with
Exception. If it doesn’t, the compiler rejects the declaration. - No explicit constructor needed — the inherited constructors from
Exceptionare sufficient unless you’re adding fields.
The inherited constructors are:
new Exception()
new Exception(String message)
new Exception(Exception cause) // wraps a cause
new Exception(String message, Exception cause)
getCause() returns the wrapped exception when you chain them, which is invaluable for debugging when one failure triggers another.
Wrapping a cause
try {
insert someAccounts;
} catch (DmlException e) {
throw new OrderProcessingException(
'Failed to provision accounts for order ' + orderId, e
);
}
The new exception carries the original DmlException as its cause. Whatever logs the outer exception can walk the chain and print the original DML error too.
Common pitfalls
- Catching
Exceptionand swallowing it. Always log it, and re-throw unless you have a specific reason to suppress. Swallowed exceptions are the most painful bugs to diagnose later. - Using built-in
NullPointerExceptionfor business errors. Use a custom exception named for the domain failure, not a generic platform type. - Forgetting the
Exceptionsuffix. The compiler rejectsclass OrderInvalid extends Exception {}— must beOrderInvalidException. - Putting business rules inside catch blocks. If you find yourself doing real work in a catch, the throw site is probably wrong.
What interviewers are really looking for
This question is a language-knowledge check plus a design check. The code part is short — one class, two constructors. The design signal is whether you can articulate when to define one (specific failure modes the caller might want to handle differently) vs when to let a platform exception fly. Mention the Exception suffix requirement, the inherited constructors, and chaining causes with getCause(), and you’ve covered everything an interviewer is checking for.
Verified against: Apex Developer Guide — Creating Custom Exception Classes, Exception Class and Built-In Exceptions. Last reviewed 2026-05-17 for Spring ‘26 release.