A PHP warning reports a problem that did not stop execution — the script continues, usually with a value you did not want. PHP 8 changed the severity of many of these: undefined variables and undefined array keys were promoted from notice to warning, and several old warnings became fatal Error exceptions. Code that ran quietly on PHP 7 can be loud or broken on PHP 8 without a single line changing.
The practical questions are which level to report, where the messages should go, and what the common warnings are actually telling you.
Table of contents
- The severity ladder
- Configuring what gets reported and where
- The warnings you will actually see
- Converting warnings into exceptions
- Deprecations and version upgrades
- Where the log should live
- How this fits the rest of the stack
- FAQ
The severity ladder
- Notice — minor. Something unusual but probably intentional.
- Warning — a real problem; execution continues with a fallback value.
- Deprecated — works now, will be removed. These are your upgrade to-do list.
- Fatal error — execution stops.
- Error / TypeError / ValueError — PHP 7+ throwable errors, catchable with
try.
The PHP 8 reclassifications that catch people:
<?php
// PHP 7: Notice. PHP 8: Warning.
echo $undefinedVariable;
echo $array['missing_key'];
// PHP 7: Warning, returns null. PHP 8: fatal TypeError.
strlen([]);
// PHP 7: Warning. PHP 8: DivisionByZeroError (throwable).
$x = 1 % 0;
// intdiv and modulo throw; the / operator warns and gives INF
$y = 1 / 0; // Warning + INF in PHP 8
“Undefined array key” being a warning rather than a notice is the one that floods logs after an upgrade. It is also correct — reading a key that is not there is nearly always a bug, and the null you get back propagates somewhere unhelpful.
Configuring what gets reported and where
<?php
// Development
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
// Production
error_reporting(E_ALL);
ini_set('display_errors', '0'); // never show users a stack trace
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php/app-error.log');
The rule is simple and frequently broken: display_errors off in production, error_reporting still E_ALL. Report everything, show nothing. Turning down error_reporting to quiet the logs hides bugs; leaving display_errors on shows users your file paths, your database structure in query errors, and sometimes credentials in a connection string.
ini_set in a script cannot catch parse errors, because the file failed to compile before your line ran. For those you need the setting in php.ini or the server configuration. This is why a syntax error gives a blank white page while runtime errors display fine.
; php.ini -- production
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/error.log
The warnings you will actually see
Undefined array key — reading something that is not there.
<?php
$name = $_POST['name']; // Warning if not submitted
$name = $_POST['name'] ?? ''; // null coalescing, PHP 7+
$name = $data['user']['name'] ?? ''; // works at any depth
?? is the fix in essentially every case. Note it differs from ?: — the null coalescing operator only checks for null or unset, so $x ?? 'default' keeps 0 and '' where $x ?: 'default' replaces them.
Cannot modify header information — headers already sent — output was produced before a header() or session_start() call.
<?php
// The message names the file and line that sent output first -- read it
// Warning: Cannot modify header information ... output started at /app/config.php:3
Nearly always whitespace after a closing ?> in an included file. The fix is to omit the closing tag entirely in files that contain only PHP — it is the recommended style precisely because of this.
failed to open stream — a file path or permissions problem.
<?php
if (!is_readable($path)) {
throw new RuntimeException("cannot read: $path");
}
$contents = file_get_contents($path);
Checking first turns a warning plus a false return into a clear exception at the point of failure, rather than a type error further down when something tries to use the false.
Converting warnings into exceptions
The most useful thing you can do with warnings in an application: stop letting them be ignorable.
<?php
set_error_handler(function (int $severity, string $message, string $file, int $line) {
if (!(error_reporting() & $severity)) {
return false; // respect the @ operator and error_reporting
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
Now an undefined array key raises a catchable exception with a stack trace, instead of producing a null that surfaces as a strange bug three functions away. Most frameworks do this by default, which is why they feel stricter than plain PHP.
Fatal errors need a separate hook, since they bypass the error handler:
<?php
register_shutdown_function(function () {
$e = error_get_last();
if ($e && in_array($e['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
error_log(sprintf('FATAL: %s in %s:%d', $e['message'], $e['file'], $e['line']));
}
});
And a note on @: the error suppression operator hides the message but the error still occurred, and the function still returned false or null. @file_get_contents($url) gives you a silent false that fails confusingly later. Check the return value instead.
Deprecations and version upgrades
E_DEPRECATED messages are the cheapest upgrade signal you will get, and the easiest to ignore because nothing is broken yet.
<?php
// Common PHP 8.1+ deprecation: implicitly nullable parameters
function f(string $s = null) {} // deprecated in 8.4
function f(?string $s = null) {} // explicit, correct
// Passing null to a non-nullable internal parameter
strlen(null); // deprecated in 8.1
Run with error_reporting(E_ALL) in development and staging so deprecations are visible before the version bump makes them fatal. A deprecation you fix today is a five-minute change; the same one after an upgrade is an outage.
Before any major PHP upgrade, grep the logs for Deprecated: across a representative period. That list is your migration plan, derived from code paths that actually execute rather than from static analysis guessing.
Where the log should live
A file on one server is where PHP error logs traditionally go, and it is the weakest part of the setup. It requires an SSH session to read, it is per-machine, and it is discarded when the machine is replaced.
Writing to stderr instead lets the platform collect it, which is what turns “log in and tail a file” into “look at the runtime logs for this deploy”:
; php.ini
error_log = /dev/stderr
log_errors = On
display_errors = Off
That matters most during an incident. A warning that started appearing is only useful if you can see when it started and what deploy it coincided with — the failing build and the failing request in one place, rather than correlating a log file against a deployment record by timestamp. Deploy logs and runtime logs per deploy are how RunxBuild exposes it, and for WordPress specifically the dashboard’s file manager and database browser mean checking a config value does not need an SFTP client either.
How this fits the rest of the stack
Keep error_reporting(E_ALL) everywhere and display_errors off in production. Fix undefined keys with ??, fix header warnings by removing the closing ?> tag, and check is_readable before opening files. Convert warnings to ErrorException with set_error_handler so they fail where they happen rather than three functions later.
Treat E_DEPRECATED as your upgrade checklist, and log to stderr so the platform collects the output rather than leaving it in a file on one machine. If you are working out what that service and its database cost to run, the RunxBuild hosting calculator shows them as separate line items.
Useful related references:
- Python Logging Levels: What DEBUG, INFO, WARNING, ERROR and CRITICAL Are Actually For
- What Does PHP Stand For? A Recursive Joke That Still Runs Half the Web
- The Uploaded File Exceeds the upload_max_filesize Directive: Fixing It in the Right Place
- Services on RunxBuild
FAQ
What is the difference between a warning and an error in PHP?
A warning reports a problem but execution continues, usually with a fallback value such as null or false. An error stops execution. PHP 8 moved several former warnings into throwable Error classes, so code that limped along on PHP 7 may now stop entirely.
How do I show PHP warnings?
Set error_reporting(E_ALL) and ini_set('display_errors', '1') in development. Note that ini_set cannot catch parse errors, since the file failed to compile before your line ran — those need the setting in php.ini, which is why a syntax error produces a blank page.
Should I turn off warnings in production?
Turn off display_errors so users never see them, but keep error_reporting at E_ALL and log_errors on. Reducing the reporting level hides real bugs, while displaying errors leaks file paths, database structure, and sometimes credentials to anyone who triggers one.
How do I fix ‘Undefined array key’ in PHP 8?
Use the null coalescing operator: $value = $array['key'] ?? '';. It works at any nesting depth. Unlike ?:, it only substitutes when the value is null or unset, so a legitimate 0 or empty string is preserved rather than replaced by your default.
What causes ‘Cannot modify header information - headers already sent’?
Output was produced before the header() or session_start() call. The warning names the file and line where output began — usually whitespace after a closing ?> tag in an included file. Omit the closing tag in files containing only PHP, which is the recommended style for this reason.