Skip to content
intermediate Phase 6 · PHP Advanced

PHP Exceptions: Error Handling and Custom Exceptions

Master try/catch/finally, exception hierarchy, custom exceptions, SPL exceptions, and Magento exception types.

45m
0 problems
Topic Progress 0%

Try/Catch/Finally and Exception Basics

Basic Exception Handling

<?php
// Basic try-catch
try {
    $product = $productRepository->getById($productId);
    if (!$product) {
        throw new \Exception('Product not found');
    }
    echo $product->getName();
} catch (\Exception $e) {
    echo 'Error: ' . $e->getMessage();
    echo 'File: ' . $e->getFile();
    echo 'Line: ' . $e->getLine();
}

// Multiple catch blocks (specific to general)
try {
    $data = json_decode($input, true);
    if ($data === null) {
        throw new \InvalidArgumentException('Invalid JSON');
    }
    $result = $service->process($data);
} catch (\InvalidArgumentException $e) {
    // Handle invalid input
    echo 'Invalid input: ' . $e->getMessage();
} catch (\RuntimeException $e) {
    // Handle runtime errors
    echo 'Runtime error: ' . $e->getMessage();
} catch (\Exception $e) {
    // Handle all other exceptions
    echo 'Error: ' . $e->getMessage();
}

// finally block (always executes)
try {
    $connection = new PDO($dsn, $user, $pass);
    $connection->query('SELECT * FROM products');
} catch (PDOException $e) {
    error_log('Database error: ' . $e->getMessage());
} finally {
    // Clean up regardless of success/failure
    $connection = null;
}

Exception Methods

<?php
try {
    throw new \RuntimeException('Something went wrong', 42, new \Exception('Root cause'));
} catch (\Exception $e) {
    echo $e->getMessage();    // 'Something went wrong'
    echo $e->getCode();       // 42
    echo $e->getFile();       // Full path to file where exception was thrown
    echo $e->getLine();       // Line number
    echo $e->getTraceAsString(); // Stack trace as string

    // Previous exception (chained exceptions)
    $previous = $e->getPrevious();
    if ($previous) {
        echo 'Caused by: ' . $previous->getMessage();
    }
}

Throwing Exceptions

<?php
// Throw with message
throw new \Exception('Something failed');

// Throw with message and code
throw new \InvalidArgumentException('Invalid price', 1001);

// Throw with previous exception (chaining)
try {
    $db->query('SELECT * FROM products');
} catch (PDOException $e) {
    throw new \RuntimeException('Failed to load products', 500, $e);
}

// Re-throw current exception
try {
    $service->process();
} catch (\Exception $e) {
    error_log($e->getMessage());
    throw $e;  // Re-throw for caller to handle
}

Catching Multiple Exception Types

<?php
// Single catch with multiple types (PHP 7.0+)
try {
    $service->process($data);
} catch (\InvalidArgumentException | \RuntimeException $e) {
    // Handles both types
    error_log($e->getMessage());
}

// Catch everything
try {
    riskyOperation();
} catch (\Throwable $e) {
    // Catches ALL errors and exceptions (PHP 7+)
    error_log($e->getMessage());
}

Key Takeaway

Always catch specific exceptions before general ones. Use finally for cleanup code. Chain exceptions with the previous parameter to preserve error context. Use \Throwable to catch everything including errors.

Custom Exceptions and Exception Hierarchy

Creating Custom Exceptions

<?php
namespace Vendor\Module\Exception;

// Base exception for the module
class ModuleException extends \RuntimeException
{
    // Custom code range for the module
    protected int $moduleCode = 1000;

    public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null)
    {
        parent::__construct($message, $this->moduleCode + $code, $previous);
    }
}

// Specific exceptions
class ProductNotFoundException extends ModuleException
{
    public function __construct(int $productId, ?\Throwable $previous = null)
    {
        parent::__construct(
            "Product with ID $productId not found",
            1, // Product not found code
            $previous
        );
    }
}

class InvalidProductDataException extends ModuleException
{
    public function __construct(string $field, mixed $value, ?\Throwable $previous = null)
    {
        parent::__construct(
            "Invalid value for field '$field': " . var_export($value, true),
            2, // Invalid data code
            $previous
        );
    }
}

class InsufficientStockException extends ModuleException
{
    public function __construct(int $requested, int $available, ?\Throwable $previous = null)
    {
        parent::__construct(
            "Insufficient stock: requested $requested, available $available",
            3, // Insufficient stock code
            $previous
        );
    }
}

Using Custom Exceptions

<?php
namespace Vendor\Module\Service;

use Vendor\Module\Exception\ProductNotFoundException;
use Vendor\Module\Exception\InvalidProductDataException;
use Vendor\Module\Exception\InsufficientStockException;

class ProductValidationService
{
    public function validateProduct(array $data): void
    {
        if (empty($data['name'])) {
            throw new InvalidProductDataException('name', $data['name'] ?? null);
        }

        if (!isset($data['price']) || $data['price'] <= 0) {
            throw new InvalidProductDataException('price', $data['price'] ?? null);
        }

        if (strlen($data['sku'] ?? '') > 64) {
            throw new InvalidProductDataException('sku', $data['sku']);
        }
    }

    public function checkStock(int $productId, int $requestedQty): void
    {
        $available = $this->getStockLevel($productId);

        if ($available < $requestedQty) {
            throw new InsufficientStockException($requestedQty, $available);
        }
    }
}

// Handling custom exceptions
try {
    $validator->validateProduct($productData);
    $validator->checkStock($productId, $quantity);
} catch (InvalidProductDataException $e) {
    // Show validation error to user
    $errors[] = $e->getMessage();
} catch (InsufficientStockException $e) {
    // Show stock error to user
    $errors[] = 'Not enough stock';
} catch (ModuleException $e) {
    // Handle any module-specific exception
    error_log('Module error: ' . $e->getMessage());
}

Exception Hierarchy in PHP

Throwable
  |
  +-- Exception
  |     |
  |     +-- BadFunctionCallException
  |     +-- BadMethodCallException
  |     +-- DomainException
  |     +-- InvalidArgumentException
  |     +-- LengthException
  |     +-- LogicException
  |     +-- OutOfBoundsException
  |     +-- OutOfRangeException
  |     +-- OverflowException
  |     +-- RangeException
  |     +-- RuntimeException
  |     |     |
  |     |     +-- UnexpectedValueException
  |     |     +-- UnderflowException
  |     |     +-- Symfony\Component\HttpKernel\Exception\HttpException
  |     |
  |     +-- Magento\Framework\Exception\LocalizedException
  |     |     |
  |     |     +-- Magento\Framework\Exception\NoSuchEntityException
  |     |     +-- Magento\Framework\Exception\CouldNotSaveException
  |     |     +-- Magento\Framework\Exception\InputException
  |     |     +-- Magento\Framework\Exception\AlreadyExistsException
  |     |     +-- Magento\Framework\Exception\ValidatorException
  |     |
  |     +-- PDOException
  |
  +-- Error
        |
        +-- ArithmeticError
        +-- AssertionError
        +-- DivisionByZeroError
        +-- ErrorException
        +-- ParseError
        +-- TypeError

Magento Exception Types

<?php
// LocalizedException - user-facing errors
throw new \Magento\Framework\Exception\LocalizedException(
    new \Magento\Framework\Phrase('Product not found')
);

// NoSuchEntityException - entity doesn't exist
throw new \Magento\Framework\Exception\NoSuchEntityException(
    __('Product with ID %1 not found', $productId)
);

// CouldNotSaveException - save operation failed
throw new \Magento\Framework\Exception\CouldNotSaveException(
    __('Could not save product: %1', $e->getMessage())
);

// InputException - invalid input
throw new \Magento\Framework\Exception\InputException(
    __('Invalid product data')
);

// AlreadyExistsException - entity already exists
throw new \Magento\Framework\Exception\AlreadyExistsException(
    __('Product with SKU %1 already exists', $sku)
);

Key Takeaway

Create module-specific exception hierarchies for clear error handling. Magento provides specific exception types for common scenarios. Always preserve the exception chain with the previous parameter.

Exception Best Practices and Error Handling

Exception Best Practices

<?php
// DO: Catch specific exceptions first
try {
    $product = $repo->getById($id);
} catch (NoSuchEntityException $e) {
    // Handle specific case
} catch (LocalizedException $e) {
    // Handle broader case
} catch (\Exception $e) {
    // Catch-all last
}

// DON'T: Catch everything too early
try {
    $product = $repo->getById($id);
} catch (\Exception $e) {
    // Too broad - hides specific errors
}

Exception Logging

<?php
namespace Vendor\Module\Service;

use Psr\Log\LoggerInterface;

class OrderService
{
    public function __construct(
        private LoggerInterface $logger
    ) {}

    public function processOrder(int $orderId): void
    {
        try {
            $order = $this->orderRepo->getById($orderId);
            $this->validateOrder($order);
            $this->chargePayment($order);
            $this->sendConfirmation($order);
        } catch (\Magento\Framework\Exception\LocalizedException $e) {
            // User-facing error - log and re-throw
            $this->logger->error('Order processing failed', [
                'order_id' => $orderId,
                'exception' => $e
            ]);
            throw $e;
        } catch (\Exception $e) {
            // Unexpected error - log and convert to user-friendly message
            $this->logger->critical('Unexpected error in order processing', [
                'order_id' => $orderId,
                'exception' => $e
            ]);
            throw new \Magento\Framework\Exception\LocalizedException(
                __('An error occurred while processing your order. Please try again later.')
            );
        }
    }
}

Error vs Exception

<?php
// PHP 7+ converts errors to exceptions
try {
    $result = 10 / 0; // DivisionByZeroError
} catch (\DivisionByZeroError $e) {
    error_log('Division by zero: ' . $e->getMessage());
}

try {
    $value = $undefinedVar; // Notice ->ErrorException
} catch (\ErrorException $e) {
    error_log('Undefined variable: ' . $e->getMessage());
}

// Convert legacy errors to exceptions
set_error_handler(function ($severity, $message, $file, $line) {
    throw new \ErrorException($message, 0, $severity, $file, $line);
});

Exception in Magento Controller

<?php
namespace Vendor\Module\Controller\Product;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Exception\LocalizedException;

class View extends Action
{
    public function __construct(
        Context $context,
        private JsonFactory $jsonFactory
    ) {
        parent::__construct($context);
    }

    public function execute()
    {
        try {
            $productId = (int)$this->getRequest()->getParam('id');
            $product = $this->productRepository->getById($productId);

            $result = $this->jsonFactory->create();
            $result->setData([
                'status' => true,
                'data' => $product->toArray()
            ]);
            return $result;

        } catch (NoSuchEntityException $e) {
            $result = $this->jsonFactory->create();
            $result->setData([
                'status' => false,
                'message' => __('Product not found')
            ])->setHttpResponseCode(404);
            return $result;

        } catch (LocalizedException $e) {
            $result = $this->jsonFactory->create();
            $result->setData([
                'status' => false,
                'message' => $e->getMessage()
            ])->setHttpResponseCode(400);
            return $result;

        } catch (\Exception $e) {
            $result = $this->jsonFactory->create();
            $result->setData([
                'status' => false,
                'message' => __('An error occurred')
            ])->setHttpResponseCode(500);
            return $result;
        }
    }
}

Key Takeaway

Always catch specific exceptions first, log exceptions with context, and convert technical exceptions to user-friendly messages. Use finally for cleanup. Preserve exception chains for debugging.

Quiz

1. What does the finally block do?

Question 1 options

2. What is the benefit of custom exceptions?

Question 2 options

3. What does chaining exceptions (previous parameter) do?

Question 3 options

4. Which Magento exception is used for user-facing errors?

Question 4 options

5. What does \Throwable catch in PHP 7+?

Question 5 options

Flashcards

Question

What does try/catch/finally do?

Answer

try: code that might throw. catch: handles the exception. finally: always executes (cleanup). Multiple catch blocks go from specific to general.

Question

What is exception chaining?

Answer

Passing the previous exception as the third parameter to a new exception. Preserves the original error context for debugging.

Question

What is the difference between Exception and Error in PHP 7+?

Answer

Both implement \Throwable. Exception: anticipated errors (not found, invalid input). Error: programming mistakes (undefined variable, type error).

Question

What is LocalizedException in Magento?

Answer

User-facing exception that wraps messages in Phrase objects for translation. Used for errors that should be displayed to the customer.

Question

What is NoSuchEntityException in Magento?

Answer

Thrown when a requested entity (product, order, customer) is not found in the database. Extends LocalizedException.

Question

How should you order catch blocks?

Answer

Specific to general. Catch ProductNotFoundException before LocalizedException, and LocalizedException before \Exception.

Question

What does the finally block guarantee?

Answer

Code in finally always executes, regardless of whether an exception was thrown, caught, or re-thrown. Used for cleanup like closing connections.

Question

What should you do with unexpected exceptions?

Answer

Log the full exception with context, then throw a user-friendly LocalizedException. Never expose technical details to end users.

Revision Notes

Key Takeaways

  • 1. Use try/catch/finally for exception handling
  • 2. Catch specific exceptions before general ones
  • 3. Create custom exception hierarchies for your modules
  • 4. Chain exceptions to preserve error context
  • 5. Use finally for cleanup code (closing connections, files)
  • 6. Log exceptions with context, show user-friendly messages
  • 7. Magento exceptions: LocalizedException, NoSuchEntityException, CouldNotSaveException

Interview Tips

  • Explain the difference between Exception and Error in PHP 7+
  • Describe when to use custom exceptions vs built-in ones
  • Know Magento's exception hierarchy
  • Explain exception chaining and why it's useful
  • Describe best practices for exception handling in controllers

Cheat Sheet

PHP Exceptions Cheat Sheet

Basic Syntax:

try {
    riskyCode();
} catch (SpecificException $e) {
    handleSpecific($e);
} catch (\Exception $e) {
    handleGeneral($e);
} finally {
    cleanup();
}

Exception Methods:
getMessage(), getCode(), getFile(), getLine()
getTrace(), getTraceAsString(), getPrevious()

Magento Exceptions:

  • LocalizedException: User-facing errors
  • NoSuchEntityException: Entity not found
  • CouldNotSaveException: Save failed
  • InputException: Invalid input
  • AlreadyExistsException: Duplicate entity

Best Practices:

  1. Catch specific before general
  2. Chain exceptions (previous parameter)
  3. Use finally for cleanup
  4. Log with context
  5. Show user-friendly messages