PHP failures appear through configured error reporting and the Throwable hierarchy. Exceptions and Errors unwind until a matching catch or final handler, while finally provides cleanup but can also replace earlier outcomes if misused.
Production handling separates safe responses from diagnostic logs, preserves cause chains, makes transactions and cleanup explicit, and tests failure impact instead of merely catching every Throwable.
<?php
function applyDiscount(float $total, float $percent): float
{
if ($percent < 0 || $percent > 100) {
throw new InvalidArgumentException('Percent must be between 0 and 100.');
}
return $total * (1 - $percent / 100);
}
try {
echo applyDiscount(500, 20);
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage();
}
400
Catch a specific exception when the response differs by failure type. A final Throwable catch at an application boundary can log unexpected failures and return a generic response, but domain code should not convert every defect into success.
| Failure | Possible type |
|---|---|
| Invalid function argument | InvalidArgumentException |
| Runtime resource failure | RuntimeException |
| JSON encoding or decoding failure | JsonException |
| PDO operation failure | PDOException |
| Type contract violation | TypeError |
finally runs whether the try block succeeds or throws. Use it to close resources or release application-level state that must always be cleaned up.
<?php
$handle = fopen(__FILE__, 'rb');
if ($handle === false) {
throw new RuntimeException('Could not open file.');
}
try {
echo fgets($handle);
} finally {
fclose($handle);
}
| Environment | User response | Diagnostic detail |
|---|---|---|
| Development | Detailed local error page | Display or local debugger plus logs |
| Production | Generic safe message and status | Protected logs and monitoring |
PHP reports notices, warnings, deprecations, and errors under the configured `error_reporting` mask. Development should surface all relevant issues, including deprecations before an upgrade. Production should log actionable detail while returning a generic response that does not expose paths, SQL, configuration, or stack traces.
`display_errors` controls direct output and should be disabled in production; `log_errors` controls logging. These directives are not substitutes for an application exception boundary. Configuration can vary by SAPI and loaded ini files, so verify the effective settings in web and CLI environments separately.
The `@` error-control operator changes reporting for one expression and hides evidence from normal output. Avoid it in application code. Prefer an API that exposes failure through return values or Throwable, then handle that contract explicitly.
Deprecations are advance notice of incompatible future behavior. Track them by code owner and runtime target instead of suppressing the category globally. A clean test suite under the next supported PHP version is part of upgrade readiness.
Both Exception and Error implement Throwable. Exceptions commonly represent application or library failures, while Error covers engine-level problems such as type and argument errors. Catch the narrowest type a boundary can meaningfully handle; a broad Throwable catch belongs at a final request or job boundary for reporting and cleanup.
Thrown objects move up the call stack until a matching catch is found, executing relevant finally blocks while unwinding. If no handler matches, the request or process terminates under host behavior. Do not depend on a global handler to continue an operation whose state may be partial.
A catch block may list multiple types when they have the same response. Order catches from specific to broad. Since PHP 8, a catch variable can be omitted when the object is genuinely unused, but preserving it is useful when context or a cause must be logged.
The throw keyword is an expression in PHP 8 and can appear in null-coalescing or arrow expressions. Use this concision only for a simple invariant. Complex validation deserves a named branch with field and operation context.
Keep a try block around the operation whose failure shares one handling policy. A large try that covers parsing, authorization, database work, rendering, and logging makes it unclear which state was committed and can catch programming defects under an unrelated response.
A catch may recover with a valid substitute, translate to a domain exception, add context and rethrow, or produce the final protocol response. Logging and rethrowing at every layer creates duplicates. Report once at the boundary that owns the final outcome while lower layers preserve a previous exception.
Finally runs whether the try completes, returns, or throws. Use it for locks, temporary resources, transaction cleanup, and restoring process state. Avoid returning from finally because it can replace a return selected earlier, and avoid throwing from cleanup unless that replacement behavior is deliberate.
Database transactions need explicit commit and rollback ownership. Catch Throwable around the transaction unit, roll back only when active, and rethrow or translate. A finally block can release resources but should not hide whether the business operation committed.
Create domain-specific exceptions when callers need to distinguish outcomes such as invalid transition, conflict, not found, permission denial, or temporary dependency failure. Do not create a new class for every message if no caller handles it differently.
Exception messages support developers and operations; stable codes or types support machine decisions. Keep user-facing text separate so it can be safe, localized, and appropriate to the protocol. Never expose a raw database or filesystem exception directly in an HTTP response.
When translating an exception, pass the original Throwable as `previous` so the cause chain survives. Add operation, entity, and dependency context without repeating secrets or large payloads. Preserve the first technical cause and the final business classification.
Validation errors with many independent fields often fit a structured result better than throwing on the first field. Exceptions are strongest for abandoning the current operation, while expected alternative outcomes may deserve explicit result types.
`set_error_handler` can intercept error levels that reach the custom handler and can translate selected reportable errors to ErrorException. It cannot convert every fatal condition, and the handler should respect the active reporting mask where appropriate. Install it once during bootstrap and restore prior handlers in isolated tests.
`set_exception_handler` receives an otherwise uncaught Throwable at the global boundary. Its job is to produce the final safe response or process exit and report diagnostics. It cannot resume the failed stack frame. Keep the handler simple because dependencies may already be in a bad state.
A shutdown function can inspect `error_get_last` after termination and record certain fatal failures. Memory exhaustion may leave little capacity for logging, so reserve resources or rely on process-level logs where needed. Shutdown handling cannot make a partial request successful.
Output buffering can prevent half-rendered HTML from reaching the client when a boundary fails, but API and CLI responses need their own atomic response strategy. Do not emit diagnostic text before headers or structured error output.
Test each documented exception category, translated cause, cleanup path, transaction rollback, global response mapping, and logging redaction. Force dependencies to fail before, during, and after a state change. Assert persistent state as well as the thrown type.
Logs should include a correlation ID, operation, release, safe entity identifier, exception type, message, cause chain, and stack at the reporting boundary. Redact credentials, tokens, personal data, request bodies, and database connection details. Use structured fields instead of parsing formatted prose later.
Monitor rate and impact by fingerprint, endpoint, job, dependency, and release. A falling error count can hide swallowed exceptions, so pair it with success outcomes, latency, queue depth, and business completion signals.
During debugging, reproduce with the same configuration and input class, pause where the Throwable originates, and inspect the first application frame. Do not patch the final catch until the state transition that violated its contract is understood.
0 of 2 checked
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.