PHP Error

Error levels, display vs log, custom handlers, and dealing with fatal errors.

Intermediate
Key PointsShow all errors in development; only log in production — displayed errors leak paths and SQL.

Error levels

LevelMeaningCan catch
E_ERROR致命运行时错误,脚本终止否(需 shutdown 函数)
E_WARNING警告,继续执行是
E_NOTICE提示,可能是 bug是
E_DEPRECATED使用了过时特性是
E_PARSE语法错误,无法运行否
E_ALL所有级别—

Development vs production

PHP
<?php
// 开发环境
ini_set("display_errors", "1");
ini_set("error_reporting", (string)E_ALL);

// 生产环境
// ini_set("display_errors", "0");
// ini_set("log_errors", "1");
// ini_set("error_log", "/var/log/php/error.log");

echo ini_get("display_errors") === "1" ? "错误会显示在页面上" : "错误只写日志", "\n";
echo (int)ini_get("error_reporting") === E_ALL ? "报告所有级别" : "只报告部分级别", "\n";
Output
错误会显示在页面上
报告所有级别
With display_errors on in production, any uncaught exception prints absolute paths, SQL and credentials onto the page — the first thing a pentester looks for.

Custom error handler

PHP
<?php
error_reporting(E_ALL);

set_error_handler(function (int $no, string $msg, string $file, int $line): bool {
    $level = match ($no) {
        E_WARNING, E_USER_WARNING => "WARN",
        E_NOTICE, E_USER_NOTICE   => "NOTE",
        default                   => "ERR",
    };
    printf("[%s] %s (%s:%d)\n", $level, $msg, basename($file), $line);
    return true;      // 返回 true 表示已处理,不再走默认处理
});

echo $undefined ?? "变量不存在", "\n";
trigger_error("这是一条自定义警告", E_USER_WARNING);
trigger_error("这是一条自定义提示", E_USER_NOTICE);

restore_error_handler();
Output
[WARN] 这是一条自定义警告 (error.php:14)
[NOTE] 这是一条自定义提示 (error.php:15)

echo $undefined ?? "..." triggers no notice because ?? checks existence first. The two lines come from trigger_error.

Returning false lets PHP's default handling proceed (e.g. logging). true takes over completely — including suppressing the log unless you write it yourself.

Error vs Exception

Since PHP 7, engine errors (type errors, division by zero, missing methods) throw Error; both implement Throwable.

PHP
<?php
try {
    intdiv(1, 0);
} catch (DivisionByZeroError $e) {
    echo "除零: " . $e->getMessage(), "\n";
}

try {
    $x = null;
    $x->foo();
} catch (Error $e) {
    echo "错误: " . $e->getMessage(), "\n";
}

try {
    strlen([]);
} catch (TypeError $e) {
    echo "类型: " . $e->getMessage(), "\n";
}

echo "脚本继续执行\n";
Output
除零: Division by zero
错误: Call to a member function foo() on null
类型: strlen(): Argument #1 ($str) must be of type string, array given
脚本继续执行

Catching fatal errors

E_ERROR bypasses set_error_handler. To record it, use register_shutdown_function with error_get_last().

PHP
<?php
register_shutdown_function(function (): void {
    $err = error_get_last();
    if ($err === null) {
        return;
    }
    $fatal = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
    if (in_array($err["type"], $fatal, true)) {
        // 生产环境:写日志 + 返回友好的 500 页面
        echo "发生致命错误,已记录\n";
    }
});

// 模拟:调用不存在的函数会触发 E_ERROR
// undefined_function();
echo "正常执行完毕\n";
Output
正常执行完毕

The fatal line is commented out, so only the normal output appears. When triggered, the shutdown message prints as the script terminates.

Production error pages

PHP
<?php
ini_set("display_errors", "0");
ini_set("log_errors", "1");

set_exception_handler(function (Throwable $e): void {
    error_log(sprintf("[%s] %s in %s:%d", get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()));
    http_response_code(500);
    echo "服务器开小差了,请稍后再试。";
});

throw new RuntimeException("数据库连接失败");
Output
服务器开小差了,请稍后再试。

The user sees a plain sentence; the log keeps the full stack. Keep them separate — leaking the stack is a security issue, a blank page is a UX issue.

Friends & Partners