Designing Exception Hierarchies
PHP's Exception Hierarchy
Exception (base)
├── RuntimeException
│ ├── InvalidArgumentException
│ ├── LogicException
│ │ ├── BadMethodCallException
│ │ ├── OutOfBoundsException
│ │ └── DomainException
│ ├── OutOfRangeException
│ ├── OverflowException
│ ├── UnderflowException
│ └── RangeException
├── UnexpectedValueException
└── ErrorException
Custom Exception Hierarchy for a Module
namespace Vendor\Catalog\Exception;
// Base exception for the module
abstract class CatalogException extends \Magento\Framework\Exception\LocalizedException
{
}
// Product-related exceptions
class ProductNotFoundException extends CatalogException
{
public function __construct(string $sku, ?\Throwable $previous = null)
{
parent::__construct(
__('Product with SKU "%1" not found', $sku),
$previous,
0 // error code
);
}
}
class DuplicateSkuException extends CatalogException
{
public function __construct(string $sku, ?\Throwable $previous = null)
{
parent::__construct(
__('Product with SKU "%1" already exists', $sku),
$previous
);
}
}
// Price-related exceptions
class InvalidPriceException extends CatalogException
{
public function __construct(float $price, string $reason, ?\Throwable $previous = null)
{
parent::__construct(
__('Invalid price %1: %2', $price, $reason),
$previous
);
}
}
// Hierarchy allows catching by level:
// catch (CatalogException $e) {} — catches ALL module exceptions
// catch (ProductNotFoundException $e) {} — catches only product not found
Exception Hierarchy Rules
- Base class per module:
CatalogException,SalesException, etc. - Specific subclasses for specific errors:
ProductNotFoundException,DuplicateSkuException - Always accept previous exception: enables exception chaining
- Use meaningful messages: include context (SKU, ID, etc.)
Exception Chaining
What is Exception Chaining?
When catching one exception and throwing another, pass the original as $previous to preserve the stack trace:
namespace Vendor\Catalog\Model\Repository;
class ProductRepository
{
public function save(\Magento\Catalog\Api\Data\ProductInterface $product)
{
try {
$this->resource->save($product);
} catch (\Magento\Framework\DB\Adapter\DeadlockException $e) {
// Wrap low-level DB exception in domain exception
throw new \Vendor\Catalog\Exception\ProductSaveException(
__('Failed to save product %1', $product->getSku()),
$e // Chain: preserves original stack trace
);
} catch (\Exception $e) {
throw new \Vendor\Catalog\Exception\ProductSaveException(
__('Unexpected error saving product'),
$e
);
}
}
}
// Usage: catch at appropriate level
try {
$repo->save($product);
} catch (\Vendor\Catalog\Exception\ProductSaveException $e) {
// Domain-level error
$this->logger->error('Save failed', ['exception' => $e]);
// Original DB exception is in $e->getPrevious()
}
Accessing Previous Exceptions
try {
$service->process();
} catch (\Exception $e) {
echo $e->getMessage(); // 'Failed to save product'
$previous = $e->getPrevious();
if ($previous) {
echo $previous->getMessage(); // 'Deadlock found...'
echo get_class($previous); // DeadlockException
}
}
Magento's LocalizedException
// Magento\Framework\Exception\LocalizedException
// Provides translatable messages (using __())
throw new \Magento\Framework\Exception\LocalizedException(
__('The product price must be greater than %1', 0)
);
// Magento\Framework\Exception\NoSuchEntityException
// For entities that don't exist
throw new \Magento\Framework\Exception\NoSuchEntityException(
__('The product with SKU "%1" does not exist.', $sku)
);
// Magento\Framework\Exception\CouldNotSaveException
// For save failures
throw new \Magento\Framework\Exception\CouldNotSaveException(
__('Could not save the order: %1', $e->getMessage())
);
// Magento\Framework\Exception\CouldNotDeleteException
// For delete failures
throw new \Magento\Framework\Exception\CouldNotDeleteException(
__('Could not delete the review')
);
Exceptions vs Error Codes
When to Use Exceptions
// Exceptions for exceptional conditions
function placeOrder(CartInterface $cart): OrderInterface
{
if ($cart->isEmpty()) {
throw new \Magento\Framework\Exception\LocalizedException(
__('Cannot place order with empty cart')
);
}
// Exceptional: unexpected failure
try {
$paymentResult = $this->payment->process($cart);
} catch (\Exception $e) {
throw new \Magento\Framework\Exception\PaymentException(
__('Payment processing failed'),
$e
);
}
}
When to Use Error Codes/Return Values
// Error codes for expected, non-exceptional outcomes
function validateEmail(string $email): array
{
$errors = [];
if (strpos($email, '@') === false) {
$errors[] = 'email_missing_at';
}
if (strlen($email) > 255) {
$errors[] = 'email_too_long';
}
return $errors; // Not exceptional — expected validation
}
// Or use a Result object
final class ValidationResult
{
public function __construct(
private bool $isValid,
private array $errors = []
) {}
public function isValid(): bool { return $this->isValid; }
public function getErrors(): array { return $this->errors; }
}
Guidelines
| Use Exceptions | Use Return Values |
|---|---|
| File not found | Validation errors |
| Database connection failed | Business rule violations (expected) |
| Permission denied | Partial success with warnings |
| Network timeout | Status checks |
Exceptions are for exceptional conditions. Validation failures are expected and should return error information, not throw exceptions.
Quiz
1. What is the purpose of exception chaining ($previous)?
2. When should you use exceptions vs return values?
3. Magento's LocalizedException provides:
Flashcards
Question
What is exception chaining?
Click to reveal answer
Answer
Passing the original exception as $previous to preserve stack traces
Question
When to use exceptions vs return values?
Click to reveal answer
Answer
Exceptions = exceptional conditions; Return values = expected outcomes
Question
Magento LocalizedException feature?
Click to reveal answer
Answer
Translatable messages using __() for store-specific translations
Question
What is a good exception hierarchy design?
Click to reveal answer
Answer
Base class per module + specific subclasses for specific errors
Revision Notes
Key Takeaways
- 1. Design exception hierarchies: base class per module, specific subclasses
- 2. Always chain exceptions with $previous for debugging
- 3. Use meaningful messages with context (SKU, ID, etc.)
- 4. Exceptions for exceptional conditions; return values for expected outcomes
- 5. Magento provides LocalizedException, NoSuchEntityException, CouldNotSaveException, etc.
Interview Tips
- • Explain exception chaining and why $previous matters for debugging
- • Give examples: when validation returns errors vs when to throw exceptions
- • Discuss Magento's exception hierarchy and when to use each type
Cheat Sheet
Exception Hierarchy:
Module base: CatalogException extends LocalizedException
Specific: ProductNotFoundException, DuplicateSkuException
Chaining:
throw new DomainException('msg', $lowLevelException);
$e->getPrevious() → original exception
Magento Exceptions:
LocalizedException → Translatable message
NoSuchEntityException → Entity not found
CouldNotSaveException → Save failed
CouldNotDeleteException → Delete failed
Exceptions → exceptional conditions (DB error, not found)
Return values → expected outcomes (validation errors)