Skip to content

Error Handling

Ray Fung edited this page Feb 26, 2026 · 3 revisions

Error Handling

Razy provides a structured exception hierarchy rooted in PHP's built-in RuntimeException. Each subsystem throws a domain-specific exception, making it straightforward to catch errors at different granularity levels.


Table of Contents


Exception Hierarchy


RuntimeException (PHP built-in)

→?→ CacheException

→?→ ConfigurationException

→?→ ContainerException (PSR-11)

→  →?→ ContainerNotFoundException (PSR-11)

→?→ DatabaseException

→  →?→ ConnectionException

→  →?→ QueryException

→  →?→ TransactionException

→?→ FileException

→?→ HttpException

→  →?→ NotFoundException (404)

→  →?→ RedirectException (301/302)

→?→ MailerException

→?→ ModelNotFoundException

→?→ ModuleException

→  →?→ ModuleConfigException

→  →?→ ModuleLoadException

→?→ NetworkException

→  →?→ FTPException

→  →?→ SSHException

→?→ OAuthException

→?→ PipelineException

→?→ QueueException

→?→ RoutingException

→?→ TemplateException


Base Exceptions

All Razy exceptions extend RuntimeException, inheriting:

$e->getMessage();    // Human-readable message

$e->getCode();       // Error code (int)

$e->getPrevious();   // Wrapped previous exception

$e->getFile();       // File where thrown

$e->getLine();       // Line where thrown

$e->getTrace();      // Stack trace array

Container Exceptions

ContainerException

Thrown for DI container resolution failures. Implements PSR-11 Psr\Container\ContainerExceptionInterface.

use Razy\Exception\ContainerException;



try {

    $service = $container->get(SomeService::class);

} catch (ContainerException $e) {

    // Binding misconfiguration, circular dependency, etc.

    echo $e->getMessage();

}

ContainerNotFoundException

Thrown when a requested binding is not found. Implements PSR-11 Psr\Container\NotFoundExceptionInterface.

use Razy\Exception\ContainerNotFoundException;



try {

    $container->get('unregistered.service');

} catch (ContainerNotFoundException $e) {

    echo "Service not registered: " . $e->getMessage();

}

Database Exceptions

DatabaseException

Base exception for all database errors.

use Razy\Exception\DatabaseException;



try {

    $db->query('SELECT * FROM non_existent_table');

} catch (DatabaseException $e) {

    // Catches Connection, Query, and Transaction errors

}

ConnectionException

Failed to connect or lost connection.

use Razy\Exception\ConnectionException;



try {

    $db->connect();

} catch (ConnectionException $e) {

    echo "Cannot connect: " . $e->getMessage();

    // Maybe retry or fail fast

}

QueryException

SQL query execution failure. Includes query context:

use Razy\Exception\QueryException;



try {

    $db->query('INSERT INTO users (email) VALUES (?)', ['duplicate@example.com']);

} catch (QueryException $e) {

    echo "Query failed: " . $e->getMessage();

    // Message typically includes the SQL and error details

}

TransactionException

Transaction commit/rollback failure:

use Razy\Exception\TransactionException;



try {

    $db->beginTransaction();

    // ... operations ...

    $db->commit();

} catch (TransactionException $e) {

    echo "Transaction error: " . $e->getMessage();

}

HTTP Exceptions

HttpException

Base HTTP error with status code:

use Razy\Exception\HttpException;



throw new HttpException('Service unavailable', 503);

NotFoundException

404 Not Found — shorthand:

use Razy\Exception\NotFoundException;



// In a controller

$user = $db->find($id);

if (!$user) {

    throw new NotFoundException("User {$id} not found");

}

RedirectException

Triggers an HTTP redirect rather than rendering a response:

use Razy\Exception\RedirectException;



// 301 Permanent redirect

throw new RedirectException('/new-url', 301);



// 302 Temporary redirect (default)

throw new RedirectException('/login');

When caught by the framework's error handler, a Location header is sent with the appropriate status code.


IO & Network Exceptions

FileException

File system operations (read, write, permissions):

use Razy\Exception\FileException;



try {

    $content = FileReader::read('/path/to/file');

} catch (FileException $e) {

    echo "File error: " . $e->getMessage();

}

NetworkException

Base for network I/O failures:

use Razy\Exception\NetworkException;



try {

    // Any FTP or SFTP operation

} catch (NetworkException $e) {

    // Catches both FTP and SSH errors

}

FTPException

FTP-specific failures (extends NetworkException):

use Razy\Exception\FTPException;



try {

    $ftp->upload('/local/file', '/remote/path');

} catch (FTPException $e) {

    echo "FTP error: " . $e->getMessage();

}

SSHException

SFTP/SSH-specific failures (extends NetworkException):

use Razy\Exception\SSHException;



try {

    $sftp->loginWithPassword('user', 'wrong-password');

} catch (SSHException $e) {

    echo "SSH error: " . $e->getMessage();

}

Module Exceptions

ModuleException

Base for module system errors:

use Razy\Exception\ModuleException;



try {

    $module->load();

} catch (ModuleException $e) {

    // Config or load error

}

ModuleConfigException

Module configuration is invalid or missing required fields:

use Razy\Exception\ModuleConfigException;



try {

    $module->loadConfig();

} catch (ModuleConfigException $e) {

    echo "Bad module config: " . $e->getMessage();

}

ModuleLoadException

Module failed to load (missing files, dependency conflicts):

use Razy\Exception\ModuleLoadException;



try {

    $distributor->loadModules();

} catch (ModuleLoadException $e) {

    echo "Module load failed: " . $e->getMessage();

}

Other Exceptions

CacheException

Cache adapter failures:

use Razy\Exception\CacheException;



try {

    Cache::get('key');

} catch (CacheException $e) {

    // Redis connection lost, file permission denied, etc.

}

ConfigurationException

Framework or application configuration errors:

use Razy\Exception\ConfigurationException;



// Thrown during bootstrap if config files are invalid

MailerException

Email sending failures:

use Razy\Exception\MailerException;



try {

    $mailer->send($message);

} catch (MailerException $e) {

    echo "Email failed: " . $e->getMessage();

    // SMTP connection error, invalid recipient, etc.

}

ModelNotFoundException

Eloquent-style "find or fail" pattern:

use Razy\Exception\ModelNotFoundException;



try {

    $user = User::findOrFail($id);

} catch (ModelNotFoundException $e) {

    $model = $e->getModel();  // 'App\Models\User'

    $ids   = $e->getIds();    // [42]

    echo "{$model} not found for IDs: " . implode(', ', $ids);

}

Methods:

  • setModel(string $model, array $ids = []): static

  • getModel(): string

  • getIds(): array

OAuthException

OAuth 2.0 flow errors:

use Razy\Exception\OAuthException;



try {

    $token = $oauth->requestAccessToken($code);

} catch (OAuthException $e) {

    echo "OAuth error: " . $e->getMessage();

}

PipelineException

Pipeline execution failures:

use Razy\Exception\PipelineException;



try {

    $pipeline->send($data)->thenReturn();

} catch (PipelineException $e) {

    echo "Pipeline error: " . $e->getMessage();

}

QueueException

Queue dispatch or worker errors:

use Razy\Exception\QueueException;



try {

    $queue->push($job);

} catch (QueueException $e) {

    echo "Queue error: " . $e->getMessage();

}

RoutingException

Route resolution or URL generation failures:

use Razy\Exception\RoutingException;



try {

    $router->resolve($request);

} catch (RoutingException $e) {

    echo "Routing error: " . $e->getMessage();

}

TemplateException

Template parsing or rendering errors:

use Razy\Exception\TemplateException;



try {

    $engine->render('template-name', $data);

} catch (TemplateException $e) {

    echo "Template error: " . $e->getMessage();

}

Usage Patterns

Granular vs. Broad Catching

// Granular → catch specific exceptions

try {

    $db->beginTransaction();

    $db->query($sql, $params);

    $db->commit();

} catch (QueryException $e) {

    $db->rollBack();

    log("SQL error: {$e->getMessage()}");

} catch (TransactionException $e) {

    log("TX error: {$e->getMessage()}");

}



// Broad → catch by subsystem

try {

    $db->beginTransaction();

    $db->query($sql, $params);

    $db->commit();

} catch (DatabaseException $e) {

    $db->rollBack();

    log("DB error: {$e->getMessage()}");

}



// Broadest → catch any Razy runtime error

try {

    // ...

} catch (\RuntimeException $e) {

    // All Razy exceptions

}

Converting Exceptions to HTTP Responses

try {

    $result = $controller->handle($request);

} catch (NotFoundException $e) {

    return new Response(404, 'Not Found');

} catch (RedirectException $e) {

    return new Response($e->getCode(), '', ['Location' => $e->getMessage()]);

} catch (HttpException $e) {

    return new Response($e->getCode(), $e->getMessage());

} catch (\RuntimeException $e) {

    return new Response(500, 'Internal Server Error');

}

Re-throwing with Context

try {

    $db->query($sql, $params);

} catch (QueryException $e) {

    throw new \RuntimeException(

        "Failed to process order #{$orderId}",

        previous: $e

    );

}

API Reference

| Exception | Parent | HTTP Code | Key Feature |

| --- | --- | --- | --- |

| CacheException | RuntimeException | — | Cache adapter errors |

| ConfigurationException | RuntimeException | — | Config file issues |

| ContainerException | RuntimeException | — | PSR-11 ContainerExceptionInterface |

| ContainerNotFoundException | ContainerException | — | PSR-11 NotFoundExceptionInterface |

| ConnectionException | DatabaseException | — | Database connection |

| DatabaseException | RuntimeException | — | Base DB error |

| FileException | RuntimeException | — | File I/O |

| FTPException | NetworkException | — | FTP operations |

| HttpException | RuntimeException | Any | HTTP status code |

| MailerException | RuntimeException | — | Email sending |

| ModelNotFoundException | RuntimeException | 404 | setModel() / getModel() / getIds() |

| ModuleConfigException | ModuleException | — | Module config |

| ModuleException | RuntimeException | — | Base module error |

| ModuleLoadException | ModuleException | — | Module loading |

| NetworkException | RuntimeException | — | Base network I/O |

| NotFoundException | HttpException | 404 | Page not found |

| OAuthException | RuntimeException | — | OAuth flow |

| PipelineException | RuntimeException | — | Pipeline execution |

| QueryException | DatabaseException | — | SQL execution |

| QueueException | RuntimeException | — | Job dispatch |

| RedirectException | HttpException | 301/302 | HTTP redirect |

| RoutingException | RuntimeException | — | Route resolution |

| SSHException | NetworkException | — | SFTP/SSH operations |

| TemplateException | RuntimeException | — | Template rendering |

| TransactionException | DatabaseException | — | TX commit/rollback |


Error System Internals

Beyond the exception hierarchy, Razy provides three core classes that manage how errors are captured, configured, and rendered.

Error Class

The Error class extends \Exception and serves as the primary error object for framework-level errors (404 pages, uncaught exceptions).

use Razy\Error;



// Create a custom error

$error = new Error(

    message: 'Resource not found',

    statusCode: 404,

    heading: 'Not Found',

    debugMessage: 'The requested URL /foo/bar was not found.',

);



$error->getStatusCode();    // 404

$error->getHeading();       // 'Not Found'

$error->getDebugMessage();  // Visible only in debug mode



// Static helpers

Error::show404();            // Throw NotFoundException (renders 404 page)

Error::showException($e);   // Render an exception (web or CLI)

Show404

// In a controller or routing callback

if (!$page) {

    Error::show404();

    // Throws NotFoundException internally

    // Renders the 404 template or JSON response

}

Debug Console

// Write to the debug console buffer (HTML debug panel)

Error::debugConsoleWrite('Query took 45ms', 'sql');

Error::debugConsoleWrite('Cache miss for key: user:42', 'cache');

ErrorConfig

ErrorConfig is a static state manager that controls error behaviour globally.

use Razy\ErrorConfig;



// Enable/disable debug mode

ErrorConfig::setDebug(true);



// Check debug state

ErrorConfig::isDebug();  // true



// Enable cached error pages

ErrorConfig::setCached(true);



// Configure all settings at once

ErrorConfig::configure([

    'debug'        => true,

    'cached'       => false,

    'display_errors' => true,

]);



// Get the accumulated debug console output

$console = ErrorConfig::getDebugConsole();



// Reset to defaults

ErrorConfig::reset();

ErrorRenderer

ErrorRenderer handles the visual output of errors. It operates differently in CLI vs web mode and supports debug backtraces in development.

use Razy\ErrorRenderer;



// Render a 404 page

ErrorRenderer::show404();

// In web mode: loads template from asset/exception/404.html

// In CLI mode: prints plain text



// Render an exception with full backtrace

ErrorRenderer::showException($exception);

// In debug mode: full backtrace with source code preview

// In production: generic error page



// Template resolution order:

// 1. PHAR_PATH/asset/exception/{statusCode}.html  (e.g., 404.html, 500.html)

// 2. PHAR_PATH/asset/exception/default.html

// 3. Fallback plain HTML

Debug Backtrace in Development

When ErrorConfig::isDebug() is true, exceptions are rendered with:

  • Exception class name and message

  • Full stack trace with file paths and line numbers

  • Source code preview around the error line

  • Debug console output (SQL queries, cache operations, etc.)

Production Error Pages

When debug mode is off, errors display a generic user-friendly message without exposing internals:

ErrorConfig::setDebug(false);



// Now exceptions show:

// "An unexpected error occurred. Please try again later."

// No stack trace, no file paths, no SQL queries

Complete Exception Flow Example

use Razy\Error;

use Razy\ErrorConfig;



// Bootstrap

ErrorConfig::setDebug(getenv('APP_DEBUG') === 'true');



// Register global exception handler

set_exception_handler(function (\Throwable $e) {

    // Log the error

    error_log($e->getMessage() . "\n" . $e->getTraceAsString());

    

    // Render appropriate response

    if ($e instanceof NotFoundException) {

        Error::show404();

    } elseif ($e instanceof HttpException) {

        http_response_code($e->getCode());

        Error::showException($e);

    } else {

        http_response_code(500);

        Error::showException($e);

    }

});

← Previous: Worker Lifecycle

Clone this wiki locally