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?
2. What is the benefit of custom exceptions?
3. What does chaining exceptions (previous parameter) do?
4. Which Magento exception is used for user-facing errors?
5. What does \Throwable catch in PHP 7+?
Flashcards
Question
What does try/catch/finally do?
Click to reveal answer
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?
Click to reveal answer
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+?
Click to reveal answer
Answer
Both implement \Throwable. Exception: anticipated errors (not found, invalid input). Error: programming mistakes (undefined variable, type error).
Question
What is LocalizedException in Magento?
Click to reveal answer
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?
Click to reveal answer
Answer
Thrown when a requested entity (product, order, customer) is not found in the database. Extends LocalizedException.
Question
How should you order catch blocks?
Click to reveal answer
Answer
Specific to general. Catch ProductNotFoundException before LocalizedException, and LocalizedException before \Exception.
Question
What does the finally block guarantee?
Click to reveal answer
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?
Click to reveal answer
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:
- Catch specific before general
- Chain exceptions (previous parameter)
- Use finally for cleanup
- Log with context
- Show user-friendly messages