Skip to content
intermediate Phase 11 · PHP Engineering Practices

Exception Design Patterns

Exception hierarchy design, custom exception chains, exception vs error codes, and Magento exception patterns

45m
0 problems
Topic Progress 0%

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

  1. Base class per module: CatalogException, SalesException, etc.
  2. Specific subclasses for specific errors: ProductNotFoundException, DuplicateSkuException
  3. Always accept previous exception: enables exception chaining
  4. 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)?

Question 1 options

2. When should you use exceptions vs return values?

Question 2 options

3. Magento's LocalizedException provides:

Question 3 options

Flashcards

Question

What is exception chaining?

Answer

Passing the original exception as $previous to preserve stack traces

Question

When to use exceptions vs return values?

Answer

Exceptions = exceptional conditions; Return values = expected outcomes

Question

Magento LocalizedException feature?

Answer

Translatable messages using __() for store-specific translations

Question

What is a good exception hierarchy design?

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)