Quickly & Reliably Handle Errors in PHP Code
Robust error handling for cleaner PHP code
Originally published: December 16th, 2017. Updated on: April 29th, 2025.
When you’re building something in PHP, whether a simple website or a larger web application, you always need to be on the lookout for errors cropping up. Nasty little bugs can creep in when you least expect them.
An error happens whenever PHP tries to execute an instruction that results in an impossible outcome or otherwise prevents the script from running as intended. Errors range from simple fatal errors (halting execution, often due to syntax mistakes or calling non-existent functions) to more complex issues where the cause isn't immediately clear, requiring an in-depth code review.
Undefined Variables & Other Little Glitches
Dealing with errors gracefully is a major consideration in PHP development. It’s important that when something goes wrong (and it likely will, eventually), your users aren’t greeted with an unsightly page of technical jargon.
PHP's default error output is often simple text with basic formatting – quite ugly, really.
<?php
// Trying to use a variable that hasn't been defined
echo ($hello_world);
?>
This might produce output like:
Notice: Undefined variable: hello_world in C:\path\to\your\php.php on line 5
You might not see this right away if your PHP configuration isn't set to display all errors. To enable full error reporting for development:
<?php
// Report all errors, including strict standards
error_reporting(E_ALL);
// Display errors directly on screen (DISABLE ON PRODUCTION!)
ini_set('display_errors', 1); // Use 1 for TRUE
echo ($hello_world);
?>
The first line tells PHP to report everything. The second line instructs PHP to display errors directly (this value is also in php.ini
, but setting it here is useful if you can't access php.ini
). Remember to turn display_errors
OFF on live production servers!
One way to avoid the "Undefined variable" notice is to check if a variable exists before using it, often with isset()
:
<?php
// Check if $hello_world is set before echoing
if (isset($hello_world)) {
echo $hello_world;
} else {
echo "The variable 'hello_world' couldn't be located.";
}
?>
But doing this for every variable isn't practical, and the custom message still reveals an internal issue.
Ugly Errors? Prettify Them With Custom Error Handling
A common fatal error message looks like this:
Fatal error: Call to undefined function hello_world() in C:\path\to\your\php.php on line 1
This isn't pretty. A visitor seeing this would likely be discouraged. A better approach is using a custom error handler. This lets you intercept PHP's default error handling and replace it with your own logic – perhaps showing a user-friendly message, logging the error for yourself, or both.
You create a custom error handler function that accepts up to five arguments:
$level
(int, required): The error level (e.g.,E_WARNING
,E_NOTICE
).$message
(string, required): The error message.$file
(string, optional): The filename where the error occurred.$line
(int, optional): The line number where the error occurred.$context
(array, optional): An array of all variables existing in scope when the error occurred (use with caution).
Common error levels include:
E_WARNING
(2): Non-fatal runtime warning. Script continues.E_NOTICE
(8): Runtime notice. Might indicate an error or something normal during script execution. Script continues.E_USER_ERROR
(256): Fatal user-generated error (triggered bytrigger_error()
). Halts script unless handled.E_USER_WARNING
(512): Non-fatal user-generated warning. Script continues.E_USER_NOTICE
(1024): User-generated notice. Script continues.E_RECOVERABLE_ERROR
(4096): Catchable fatal error. Can potentially be handled by the error handler (e.g., to log it) or usingtry...catch
with Exceptions (covered later).E_DEPRECATED
(8192): Runtime notice about using deprecated features.E_USER_DEPRECATED
(16384): User-generated deprecation notice.
Let's create a simple custom error function:
<?php
/**
* Custom error handler function.
*
* @param int $level The error level.
* @param string $message The error message.
* @param string $file The file where the error occurred.
* @param int $line The line number where the error occurred.
* @return bool True to prevent default PHP error handler, false otherwise.
*/
function myCustomErrorHandler(int $level, string $message, string $file = '', int $line = 0): bool
{
// Note: The final three arguments are optional in the function signature
// but are usually provided by PHP when the handler is called.
// Simple user-friendly message
echo "Oops! Something went wrong on our end.<br>";
echo "Please contact the site administrator at admin@example.com and let them know.<br>";
// Optionally, log the detailed error for the developer (see next section)
// error_log("PHP Error [$level]: $message in $file on line $line");
// For notices and warnings, we might let PHP handle them if needed,
// but for this example, we'll handle everything and prevent default handling.
// Returning true prevents the standard PHP error handler from running.
return true;
}
// Set our function as the default error handler
set_error_handler("myCustomErrorHandler");
// Trigger the undefined variable notice again
echo ($hello_world);
?>
This now produces a friendlier message for the user:
Oops! Something went wrong on our end.
Please contact the site administrator at admin@example.com and let them know.
This is better, but ideally, visitors shouldn't see any error messages. We want to shield them while getting detailed information for ourselves.
Logging Errors with error_log()
PHP's error_log()
function is invaluable for recording error details without displaying them to users. You can log errors to the system logger, send them via email, or write them to a file.
Let's modify our handler to log errors to a file:
<?php
function myCustomErrorHandlerWithLogging(int $level, string $message, string $file = '', int $line = 0): bool
{
// Define the path to your log file
$log_file = __DIR__ . '/php_errors.log'; // Log in the same directory as this script
// Format the log message
$error_message = sprintf(
"[%s] PHP Error Level %d: %s in %s on line %d\n",
date("Y-m-d H:i:s"), // Add timestamp
$level,
$message,
$file,
$line
);
// Log the error to the specified file (message type 3)
// Ensure the web server has write permissions for this file/directory!
error_log($error_message, 3, $log_file);
// Optionally, display a generic error message to the user ONLY for critical errors
if ($level === E_ERROR || $level === E_USER_ERROR || $level === E_RECOVERABLE_ERROR) {
echo "Sorry, a critical error occurred. We've been notified and are looking into it.";
}
// Prevent default PHP error handler
return true;
}
// Set the new handler
set_error_handler("myCustomErrorHandlerWithLogging");
// Trigger the error again
echo ($hello_world); // This will now be logged
// Trigger a user error
trigger_error("This is a custom user warning", E_USER_WARNING); // Also logged
// Trigger a fatal user error
// trigger_error("This is a fatal user error", E_USER_ERROR); // This would halt script if not handled
?>
Now, when $hello_world
is accessed, nothing appears on screen (unless it's a critical error type as defined in the if
statement), but the details are appended to php_errors.log
:
[2025-04-29 10:12:00] PHP Error Level 8: Undefined variable $hello_world in /path/to/your/script.php on line X
[2025-04-29 10:12:00] PHP Error Level 512: This is a custom user warning in /path/to/your/script.php on line Y
Important: Logging every notice (like undefined variables) can be resource-intensive on busy sites. Consider logging only more severe errors in production.
Selective Error Handling
You can make your handler smarter by acting differently based on the error level:
<?php
function selectiveErrorHandler(int $level, string $message, string $file = '', int $line = 0): bool
{
$log_file = __DIR__ . '/php_errors.log';
$error_message = sprintf(
"[%s] PHP Error Level %d: %s in %s on line %d\n",
date("Y-m-d H:i:s"), $level, $message, $file, $line
);
switch ($level) {
case E_ERROR:
case E_USER_ERROR:
case E_RECOVERABLE_ERROR:
// Log critical errors and show generic message
error_log($error_message, 3, $log_file);
echo "A critical error occurred. Please try again later.";
// Optionally exit for fatal errors if needed, though set_error_handler
// might not always prevent shutdown for true E_ERROR.
// exit(1);
break;
case E_WARNING:
case E_USER_WARNING:
// Log warnings, but don't show user message
error_log($error_message, 3, $log_file);
break;
case E_NOTICE:
case E_USER_NOTICE:
case E_DEPRECATED:
case E_USER_DEPRECATED:
// Log notices/deprecated only if specifically enabled for logging
// Or just ignore them in production
// error_log($error_message, 3, $log_file);
break;
default:
// Log unknown error types
error_log($error_message, 3, $log_file);
break;
}
// Prevent default PHP error handler for handled types
return true;
}
set_error_handler("selectiveErrorHandler");
// Example: Trigger a user notice (might be ignored by the handler above)
trigger_error("Just an informational notice", E_USER_NOTICE);
?>
Fatal Errors: Remember, set_error_handler
cannot reliably handle all fatal errors (like E_ERROR
from syntax mistakes or calling truly undefined functions). These often halt the script before the handler can fully execute or prevent shutdown. Using register_shutdown_function()
is another technique to catch some fatal errors for logging purposes, but proper coding and testing are the best prevention.
Introducing: Debug Mode
During development, seeing errors immediately is useful. But you don't want visitors seeing them. You can create a "debug mode" for your error handler.
Debug Using Your IP Address
You can show detailed errors only if the request comes from your specific IP address.
<?php
function ipBasedErrorHandler(int $level, string $message, string $file = '', int $line = 0): bool
{
$developer_ip = "YOUR_IP_ADDRESS_HERE"; // Replace with your actual IP
$log_file = __DIR__ . '/php_errors.log';
$error_message_log = sprintf( /* ... as before ... */ );
$error_message_display = "PHP Error [$level]: $message in $file on line $line";
// Check if the request IP matches the developer's IP
if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === $developer_ip) {
// Show detailed error to the developer
echo "<pre>" . htmlspecialchars($error_message_display) . "</pre>";
} else {
// Log critical errors for normal users
if ($level >= E_USER_ERROR) { // Example threshold
error_log($error_message_log, 3, $log_file);
// Optionally show generic message
// echo "An error occurred.";
}
}
return true;
}
set_error_handler("ipBasedErrorHandler");
// Get your IP (be careful with proxies/load balancers)
// echo "Your IP: " . $_SERVER['REMOTE_ADDR'];
echo ($hello_world);
?>
Note: Getting the correct visitor IP can be tricky behind proxies or load balancers ($_SERVER['HTTP_X_FORWARDED_FOR']
might be needed). This method is less reliable if your IP changes often.
Debug Using $_GET
Variables
A more flexible way is to use a URL parameter (e.g., ?debug=1
).
<?php
function getBasedErrorHandler(int $level, string $message, string $file = '', int $line = 0): bool
{
$is_debug_mode = isset($_GET['debug']) && $_GET['debug'] === '1';
$log_file = __DIR__ . '/php_errors.log';
$error_message_log = sprintf( /* ... as before ... */ );
$error_message_display = "PHP Error [$level]: $message in $file on line $line";
if ($is_debug_mode) {
// Show detailed error if debug mode is on
echo "<pre>" . htmlspecialchars($error_message_display) . "</pre>";
} else {
// Log critical errors for normal users
if ($level >= E_USER_ERROR) { // Example threshold
error_log($error_message_log, 3, $log_file);
}
}
return true;
}
set_error_handler("getBasedErrorHandler");
echo ($hello_world);
?>
Now, visit yourscript.php
normally to see no error (but it gets logged if critical). Visit yourscript.php?debug=1
to see the detailed error message directly. This makes it easy to switch between developer view and user view.
Taking An Exception: A More Modern Approach
While custom error handlers are useful, modern PHP often favors Exceptions for handling errors, especially within object-oriented code. Exceptions provide a more structured way to deal with errors and allow for graceful recovery.
An Exception is an object representing an error or unexpected event. When an exceptional situation occurs, code can throw
an Exception. This immediately stops normal execution flow and looks for a catch
block that can handle that type of Exception.
Using try...catch
for Runtime Errors
Let's rewrite a function to throw an Exception if input is invalid:
<?php
/**
* Example function that throws an exception on invalid input.
*
* @param int $v Value to check.
* @return int The value multiplied by 2 if valid.
* @throws InvalidArgumentException If value is not between 1 and 6.
*/
function checkValue(int $v): int
{
if ($v < 1 || $v > 6) {
// Throw a specific type of exception
throw new InvalidArgumentException("The value ({$v}) is not between 1 and 6 inclusive.");
}
return $v * 2;
}
try {
echo "Trying value 4: " . checkValue(4) . "<br>"; // This works
echo "Trying value 10: " . checkValue(10) . "<br>"; // This throws an exception
echo "This line will not be reached if an exception is thrown above.<br>";
} catch (InvalidArgumentException $e) {
// Catch the specific exception type
echo "Caught Exception: " . htmlspecialchars($e->getMessage()) . "<br>";
// $e object contains more info: getFile(), getLine(), getTraceAsString() etc.
// Log the error here if needed: error_log($e);
} catch (Exception $e) {
// Catch any other general Exception types (optional fallback)
echo "Caught generic Exception: " . htmlspecialchars($e->getMessage()) . "<br>";
} finally {
// This block executes regardless of whether an exception was caught or not
echo "Execution finished.<br>";
}
echo "Script continues after try-catch block.<br>";
?>
Output:
Trying value 4: 8
Caught Exception: The value (10) is not between 1 and 6 inclusive.
Execution finished.
Script continues after try-catch block.
Explanation:
- The
try
block contains code that might throw an Exception. - When
checkValue(10)
is called, itthrow
s anInvalidArgumentException
. - Execution jumps immediately to the
catch (InvalidArgumentException $e)
block. - The code inside the
catch
block runs, accessing the Exception object ($e
) to get the message. - The
finally
block runs afterwards. - Execution then continues after the
try...catch...finally
structure.
Using specific Exception types (like InvalidArgumentException
, RuntimeException
, TypeError
, or custom ones you define) allows for more granular error handling.
Using Exceptions in Classes
Exceptions are particularly useful in classes to signal errors deep within methods. The try...catch
block can be placed higher up in the call stack to handle errors originating from objects.
<?php
class Calculator
{
public function divide(float $a, float $b): float
{
if ($b == 0) {
throw new DivisionByZeroError("Cannot divide by zero."); // Specific Error type
}
if ($a < 0 || $b < 0) {
throw new InvalidArgumentException("Only positive numbers allowed.");
}
return $a / $b;
}
}
$calc = new Calculator();
try {
echo "10 / 2 = " . $calc->divide(10, 2) . "<br>";
// echo "10 / 0 = " . $calc->divide(10, 0) . "<br>"; // Throws DivisionByZeroError
echo "-10 / 2 = " . $calc->divide(-10, 2) . "<br>"; // Throws InvalidArgumentException
} catch (DivisionByZeroError $e) {
echo "Error: " . htmlspecialchars($e->getMessage()) . "<br>";
// Log specifically: error_log("Division by zero attempt: " . $e);
} catch (InvalidArgumentException $e) {
echo "Input Error: " . htmlspecialchars($e->getMessage()) . "<br>";
// Log specifically: error_log("Invalid argument: " . $e);
} catch (Throwable $e) { // Catch any remaining Error or Exception (PHP 7+)
echo "An unexpected error occurred: " . htmlspecialchars($e->getMessage()) . "<br>";
// Generic logging: error_log("Unexpected Throwable: " . $e);
}
echo "Calculation attempt finished.<br>";
?>
The Exception/Throwable Object ($e
)
The caught object ($e
in the examples, an instance of Exception
or Throwable
in PHP 7+) provides useful methods:
getMessage()
: Gets the error message string.getCode()
: Gets the error code (often 0 unless specified when thrown).getFile()
: Gets the file where the exception was thrown.getLine()
: Gets the line number where the exception was thrown.getTrace()
: Returns an array representing the call stack leading up to the exception.getTraceAsString()
: Returns the call stack trace as a formatted string.getPrevious()
: Gets the previous Exception if it was chained.__toString()
: Often provides a formatted string representation of the exception including trace.
Using var_dump($e->getTrace())
or echo $e->getTraceAsString();
inside a catch
block is invaluable for debugging.
Combining Approaches
You can combine set_error_handler
and Exceptions. Your error handler can be set to convert certain PHP errors (like Warnings or Notices) into ErrorException
objects, which can then be caught by try...catch
blocks. This allows for more consistent error handling logic.
<?php
// Error handler that throws ErrorException for warnings/notices
set_error_handler(function(int $level, string $message, string $file = '', int $line = 0) {
// Throw only for error levels included in error_reporting
if (!(error_reporting() & $level)) {
return false; // Respect error_reporting level
}
throw new ErrorException($message, 0, $level, $file, $line);
});
error_reporting(E_ALL); // Report everything
try {
echo $undefined_variable; // This Notice will be caught as an ErrorException
} catch (ErrorException $e) {
echo "Caught ErrorException: " . htmlspecialchars($e->getMessage());
echo " Severity: " . $e->getSeverity(); // E_NOTICE (8)
// Log $e here...
}
restore_error_handler(); // Restore previous handler when done
?>
A Summary
Handling errors effectively is crucial for building reliable PHP applications.
- Use
error_reporting(E_ALL)
andini_set('display_errors', 1)
during development. - Use
ini_set('display_errors', 0)
andini_set('log_errors', 1)
in production. - A custom error handler (
set_error_handler
) allows you to control how errors are presented or logged, shielding users from raw PHP errors. - Exceptions (
try...catch
,throw
) provide a structured, object-oriented way to handle exceptional conditions, especially useful in complex applications and classes. - Combining these techniques (e.g., converting errors to exceptions) can create a robust error handling strategy.
Investing time in solid error handling saves countless hours of debugging later and ensures a better experience for your users, even when things inevitably go wrong behind the scenes. It's just good practice.