Parse errors, fatal errors, warnings and uncaught exceptions explained, the 15 PHP errors every beginner hits with real messages, and the tools to debug them.
Tell a parse error, a fatal error, a warning and an uncaught exception apart from the first word of the message
Read a PHP error message and stack trace to find the line and the call that caused it
Recognise and fix the 15 errors PHP beginners hit most, from undefined array keys to headers already sent
Inspect values with var_dump, print_r, var_export and error_log, and step through code with Xdebug
Configure display_errors, error_reporting and log_errors correctly for development and production
01
Parse errors, fatal errors, warnings and exceptions
✓
PHP reports problems in four ways, and the first word of the message tells you which one you have. A parse error means the file is not valid PHP, so nothing in it runs — not even the lines above the mistake. A fatal error stops the script at that point. A warning, notice or deprecation is reported and the script keeps going, usually with a null where a value should be — which is why a warning often causes a wrong result or a fatal error a few lines later. An uncaught exception is printed as Fatal error: Uncaught ..., but unlike the others it could have been caught (Module 08).
An engine-level failure: out of memory, time limit, redeclared function
Stops
No
Warning:
Something went wrong, PHP guessed and carried on
Yes
Only by converting it (ErrorException)
Notice: / Deprecated:
Minor issue / will stop working in a future PHP
Yes
Only by converting it
Warnings are easy to miss in a busy page. A custom error handler can collect them instead of printing them — the same hook frameworks use to turn them into exceptions or send them to a log:
total = 20, name = ''
4 problems:
Warning: Undefined array key "ink" (line 15)
Warning: Undefined variable $undefinedName (line 16)
Deprecated: strtoupper(): Passing null to parameter #1 ($string) of type string is deprecated (line 16)
Deprecated: trim(): Passing null to parameter #1 ($string) of type string is deprecated (line 17)
Three lines, four problems, and the script still printed a confident-looking result. Every one of them is a bug. In development, keep error_reporting(E_ALL) and fix warnings as seriously as fatal errors.
02
Reading an error message and a stack trace
✓
Every PHP error message has the same shape: type, message, file and line. For an uncaught exception the location is in main.php:12 — the line where it was thrown — followed by a stack trace: the chain of calls that led there, innermost first (#0) down to {main}, the top-level script. Each frame reads "this file and line called this function with these arguments". Since PHP 8.5 even engine fatal errors (out of memory, redeclared function) print a stack trace.
phpmain.php
123456789101112131415161718192021222324
<?php
declare(strict_types=1);functionloadPrice(array $product):float{return $product['price'];// line 6: price is a string here}functioncartTotal(array $cart):float{
$sum =0.0;foreach($cart as $product){
$sum +=loadPrice($product);// line 13}return $sum;}try{echocartTotal([['price'=>9.5],['price'=>'12.00']]);// line 19}catch(TypeError $e){echoget_class($e),": ", $e->getMessage(),"\n";echo"thrown at line ", $e->getLine(),"\n";echostr_replace(__DIR__ . DIRECTORY_SEPARATOR,'', $e->getTraceAsString()),"\n";}
Outputcompiled & run with real PHP
TypeError: loadPrice(): Return value must be of type float, string returned
thrown at line 6
#0 main.php(13): loadPrice(Array)
#1 main.php(19): cartTotal(Array)
#2 {main}
getTraceAsString() prints full paths; the example strips the folder so the output is the same on every machine.
1
Read the first line
Type and message: TypeError, "return value must be float, string returned". That is what went wrong.
2
Go to the reported line
Line 6, the return in loadPrice(). That is where PHP noticed.
3
Walk down the trace
#0: line 13 in cartTotal() called loadPrice. #1: line 19 called cartTotal. Stop at the first frame whose data you control.
4
Find where the bad value was born
The string '12.00' came from the cart built on line 19 — that is the real bug, two frames away from the reported line.
In a framework, skip the vendor frames
A Laravel or Symfony trace can be 40 frames deep, most of them in vendor/. Scan down for the first frame in your code (app/, src/). Error pages such as Laravel's Ignition collapse vendor frames for exactly this reason.
03
Syntax and name errors
✓
The first group of errors is about the shape of your code and the names in it: a missing operator, a variable that was never set, a function or class PHP cannot find, a function declared twice. Most of them are caught before you run by an editor with PHP support or by a static analyser (last lesson).
Error you will hit
1. Parse error: syntax error, unexpected variable
php
123
<?php
$total =250;echo"Total: " $total;
Parse error: syntax error, unexpected variable "$total", expecting "," or ";" in main.php on line 3
Why PHP said that
Two values next to each other with nothing between them. After the string, PHP expected either , (another argument to echo) or ; (end of statement), and found a variable instead. For parse errors the reported line is where PHP got confused; the real mistake is often at the end of the line before (a missing ; or )).
The fix
Join the values with . or separate them with , — or interpolate.
<?php
$items =['pen','ink','paper'];foreach($items as $item){
$count++;}echo"Items: $count\n";
Warning: Undefined variable $count in main.php on line 4
Items: 3
Why PHP said that
On the first pass $count does not exist yet. PHP warns, treats it as null, and null++ gives 1, so the final answer happens to be right. The same mistake in $sum += $price or a misspelt $totl gives a wrong answer with only a warning to show for it.
The fix
Initialise every variable before you use it, and read the warning as a bug report.
php
1234567
<?php
$items =['pen','ink','paper'];
$count =0;foreach($items as $item){
$count++;}echo"Items: $count\n";// or simply count($items)
Fatal error: Uncaught Error: Call to undefined function str_join() in main.php:3
Stack trace:
#0 {main}
thrown in main.php on line 3
Why PHP said that
There is no str_join — it is a guess from another language (Python has str.join). The same error appears when the function lives in a file you forgot to require, when a PHP extension is not installed (mb_strlen without mbstring), or when you call a namespaced function without importing it.
The fix
Use the real name (implode). For your own functions, check the require/Composer autoload; for extension functions, check php -m.
php
123
<?php
$words =['php','is','fun'];echoimplode(' ', $words);// php is fun
Fatal error: Cannot redeclare function formatPrice() (previously declared in main.php:2) in main.php on line 9
Stack trace:
#0 {main}
Why PHP said that
Function names are global and must be unique. This is detected while compiling the file, so even the echo on line 7 never runs. In real projects it usually comes from including the same file twice with include instead of require_once, or from two libraries defining the same helper.
The fix
Rename one of them, use require_once, or — best — put functions in a namespace or make them static methods of a class loaded by Composer's autoloader.
Fatal error: Uncaught Error: Class "App\Controllers\DateTimeImmutable" not found in main.php:4
Stack trace:
#0 {main}
thrown in main.php on line 4
Why PHP said that
Inside a namespace, an unqualified class name is looked up in that namespace, so PHP searched for App\Controllers\DateTimeImmutable. Built-in classes live in the global namespace. The other common cause is a class in a file Composer's autoloader cannot find — a file name or namespace that does not match the PSR-4 mapping in composer.json, or a forgotten composer dump-autoload.
The fix
Import the class with use, or write it fully qualified with a leading backslash: new \DateTimeImmutable(...). The full name in the message tells you exactly what PHP looked for.
The most common PHP bugs are about data that is not there: a key missing from an array, an index one past the end, a function that returned null instead of the array or object you expected. PHP 8 made most of these louder (older versions called them "notices" and "undefined index/offset"), but the array ones are still only warnings, so the script continues with null.
Warning: Undefined array key "phone" in main.php on line 3
Phone:
Done
Why PHP said that
The key does not exist, so PHP warns and uses null (an empty string when concatenated). This is the PHP 8 name for what older tutorials call "Undefined index". It happens constantly with request data, API responses and database rows where a column is optional.
The fix
Decide what a missing value means: give a default with ??, test with isset() / array_key_exists(), or validate the array shape once at the boundary.
Score 0: 90
Score 1: 72
Score 2: 85
Warning: Undefined array key 3 in main.php on line 4
Score 3:
Why PHP said that
Three elements have indexes 0, 1 and 2. <= runs the loop a fourth time with $i = 3. With an integer key this is what older PHP called "Undefined offset". Note that the output and the warning are interleaved — the warning appears exactly when the bad line runs.
The fix
Use < with count() — or better, foreach, which cannot go past the end.
php
12345
<?php
$scores =[90,72,85];foreach($scores as $i => $score){echo"Score $i: $score\n";}
Warning: Trying to access array offset on null in main.php on line 9
Hello,
Why PHP said that
findUser(2) found nothing and returned null, and the code indexed it as if it were an array. The ?array return type was a warning sign: "this can be null".
The fix
Handle the null case explicitly before using the value, or use the null-safe forms: $user['name'] ?? 'guest'.
Fatal error: Uncaught Error: Call to a member function getEmail() on null in main.php:12
Stack trace:
#0 {main}
thrown in main.php on line 12
Why PHP said that
The object version of the previous error, and a fatal one: $order->customer is null (a guest order), and you cannot call a method on nothing. In frameworks it often means a database lookup (find(), first()) found no row.
The fix
Check for null, or use the null-safe operator ?->, which stops the chain and gives null; combine it with ?? for a default.
These are all Error subclasses PHP throws when a function gets something it cannot work with: the wrong type (TypeError), the wrong number of arguments (ArgumentCountError), a value that is the right type but impossible (ValueError), or a zero divisor (DivisionByZeroError). Their messages are precise — they name the function, the argument number and name, what was expected and what was given.
Error you will hit
10. TypeError: Argument must be of type int, string given
Fatal error: Uncaught TypeError: applyDiscount(): Argument #2 ($percent) must be of type int, string given, called in main.php on line 10 and defined in main.php:4
Stack trace:
#0 main.php(10): applyDiscount(200.0, '10')
#1 {main}
thrown in main.php on line 4
Why PHP said that
Input from a request is always a string. With strict_types=1 PHP will not convert '10' to 10 for you. The message gives both locations: the call (line 10, where the bad value was passed) and the definition (line 4). The call is where to fix it.
The fix
Validate and convert input once, at the edge of your program, and pass real types inward.
Fatal error: Uncaught ArgumentCountError: Too few arguments to function sendEmail(), 2 passed in main.php on line 7 and exactly 3 expected in main.php:2
Stack trace:
#0 main.php(7): sendEmail('[email protected]...', 'Welcome')
#1 {main}
thrown in main.php on line 2
Why PHP said that
Every parameter without a default is required. This often appears after someone adds a parameter to a function and not every caller is updated. The stack trace shortens long string arguments to 15 characters plus ....
The fix
Pass the argument, or give the new parameter a default so old callers keep working. A static analyser finds every broken call at once.
Fatal error: Uncaught DivisionByZeroError: Division by zero in main.php:4
Stack trace:
#0 {main}
thrown in main.php on line 4
Why PHP said that
An average over an empty list divides by zero. Since PHP 8.0, / by zero throws (in PHP 7 it was a warning that returned INF or false). The data is usually fine in testing and empty on a new account in production.
Fatal error: Uncaught ValueError: array_combine(): Argument #1 ($keys) and argument #2 ($values) must have the same number of elements in main.php:4
Stack trace:
#0 main.php(4): array_combine(Array, Array)
#1 {main}
thrown in main.php on line 4
Why PHP said that
A ValueError means the type was right but the value makes no sense. Here a CSV row has fewer columns than the header — a very common data problem. Other built-ins that throw it: str_repeat() with a negative count, range() with a zero step, json_decode() with depth 0.
The fix
Check the data before the call and decide how to handle the bad row: skip it, pad it, or report it.
Two last errors come from the environment rather than your logic: HTTP headers sent too late, and running out of memory or time. Both are controlled by settings you can change, but raising a limit is rarely the right fix.
Error you will hit
14. Warning: Cannot modify header information - headers already sent
Warning: session_start(): Session cannot be started after headers have already been sent (sent from main.php on line 1) in main.php on line 3
Warning: Cannot modify header information - headers already sent by (output started at main.php:1) in main.php on line 5
Why PHP said that
An HTTP response is headers first, then the body. The blank line before <?php on line 1 is output, so PHP had to send the headers right then, and later session_start() (which sets a cookie header) and header('Location: ...') can no longer add anything. The redirect silently does not happen. The part that matters is output started at main.php:1 — it tells you exactly where the stray output is. Other culprits: a UTF-8 BOM at the top of a file, whitespace after a closing ?> in an included file, or a debug echo.
The fix
Remove the output before the headers. Start files with <?php on the very first line, and leave out the closing ?> in files that contain only PHP (PSR-12 requires this).
Fatal error: Allowed memory size of 16777216 bytes exhausted (tried to allocate 1052672 bytes) in main.php on line 6
Stack trace:
#0 {main}
Why PHP said that
Every PHP request has a memory_limit (128 MB by default; here lowered to 16 MB so it fails fast). Building a million small arrays needs far more. It is an engine fatal error, not an exception: it cannot be caught. The "tried to allocate" number is just the last request that did not fit, not the total. The sibling error, Maximum execution time of 30 seconds exceeded, comes from max_execution_time in web requests (the CLI has no time limit by default).
The fix
Do not load everything at once: stream rows one by one with a generator, fetch from the database in chunks, write output as you go. Raise memory_limit only after you know the job truly needs it.
php
123456789101112131415
<?php
ini_set('memory_limit','16M');functionusers(int $n):Generator{for($i =0; $i < $n; $i++){yield['id'=> $i,'name'=>"user$i",'email'=>"[email protected]"];}}
$count =0;foreach(users(1_000_000)as $row){// one row in memory at a time
$count++;}echo $count;// 1000000
Index of the 15 errors in this module
#
Message (first words)
Kind
Usual cause
Usual fix
1
Parse error: syntax error, unexpected ...
Parse
Missing ;, ., ) or }
Look at the reported line and the one before
2
Warning: Undefined variable $x
Warning
Typo or variable never initialised
Initialise before use
3
Call to undefined function
Error
Wrong name, missing include or extension
Real name, require, php -m
4
Cannot redeclare function
Fatal
Same function twice, file included twice
require_once, namespaces
5
Class "..." not found
Error
Namespace lookup, autoload mapping
use or a leading \
6
Undefined array key "name"
Warning
Optional or missing field
??, isset()
7
Undefined array key 3
Warning
Off-by-one loop (<=)
< or foreach
8
Trying to access array offset on null
Warning
A lookup returned null
Check for null first
9
Call to a member function ... on null
Error
Object missing (no DB row, optional relation)
?->, null check
10
TypeError: must be of type int, string given
Error
Request input passed straight through
Validate and cast at the edge
11
ArgumentCountError: Too few arguments
Error
Caller not updated after a signature change
Pass it or add a default
12
DivisionByZeroError
Error
Average of an empty list
Guard the zero case
13
ValueError
Error
Right type, impossible value
Validate the data first
14
Cannot modify header information - headers already sent
Warning
Output before header() / session_start()
Remove the stray output; no closing ?>
15
Allowed memory size ... exhausted
Fatal
Loading everything into one array
Stream with generators or chunks
07
Debugging tools: var_dump, error_log, Xdebug and PHPStan
✓
The quickest debugger is a well-placed dump. var_dump() shows types and lengths (and tells "1", 1 and true apart), print_r() is shorter and easier to read for nested arrays, var_export() prints valid PHP you can paste back into a test, and json_encode() gives a one-line view that suits logs. In a web page, wrap dumps in <pre>; better still, write them to the log so users never see them.
phpmain.php
123456789101112131415
<?php
$order =['id'=>7,'paid'=>true,'total'=>'19.90','items'=>['pen',null]];var_dump($order['paid'], $order['total']);print_r($order);echo"\n";var_export($order);echo"\n",json_encode($order),"\n";// error_log type 3 appends to a file of your choice (default: the server log / stderr)
$log =tempnam(sys_get_temp_dir(),'log');error_log("order 7 total=". $order['total']."\n",3, $log);error_log("order 7 items=".json_encode($order['items'])."\n",3, $log);echofile_get_contents($log);unlink($log);
Notice what print_r hides: true prints as 1 and null as nothing, and the string "19.90" looks like a number. When a bug is about types, use var_dump.
display_errors, error_reporting and log_errors
php.ini setting
Development
Production
Why
error_reporting
E_ALL
E_ALL
Report everything; decide separately where it goes
display_errors
On
Off
Error text in a page leaks paths, SQL and secrets to visitors
log_errors
On
On
The log is how you find out about errors in production
error_log
stderr / a local file
a file or syslog
Where logged errors go
memory_limit
128M+
as needed
Per-request cap (error 15)
max_execution_time
0 in CLI
30 for web
Stops runaway requests
bash
1234
php --ini # which php.ini files are loaded
php -i | grep -E 'display_errors|error_reporting|log_errors'
php -d display_errors=1 -d error_reporting=-1 script.php # override for one run
php -l script.php # syntax check only (finds parse errors without running)
Xdebug: step debugging
Xdebug is the PHP debugger extension. With it, your editor (PhpStorm, or VS Code with the "PHP Debug" extension) can pause on a breakpoint, show every variable, and step line by line — far faster than adding dumps for anything non-trivial. It also improves var_dump output and can profile slow code. Install it only in development: it slows PHP down noticeably.
bash
12
pecl install xdebug # or: apt install php-xdebug / brew + pecl
php -v # should now say "with Xdebug v3..."
iniconf.d/99-xdebug.ini
12345
zend_extension=xdebug
xdebug.mode=debug,develop
xdebug.start_with_request=trigger ; debug only when asked (XDEBUG_TRIGGER cookie/param)
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
Start "Listen for Xdebug" in VS Code, set a breakpoint, then run XDEBUG_TRIGGER=1 php main.php (or add ?XDEBUG_TRIGGER=1 to a URL). Execution stops on your breakpoint.
Catch errors before running: PHPStan
A static analyser reads your code without running it and reports most errors in this module — undefined variables, wrong argument types and counts, calling methods on something that may be null, unknown classes. PHPStan and Psalm are the standard tools; many teams run PHPStan in CI and fail the build on new errors. Start at a low level and raise it as you fix things.
Illustrative report. PHPStan and Psalm are Composer packages, so they are shown as commands rather than runnable examples.
In real jobs
Production PHP errors go to an error tracker (Sentry, Bugsnag, Flare) that groups them, keeps the stack trace and request data, and alerts the team. When you are handed a bug, the first question is "what does the log say?" — then reproduce it locally with the same input, add a failing test, and fix. dd() ("dump and die") in Laravel and dump() in Symfony are the framework versions of var_dump; never commit them.
Parse error
The file is not valid PHP syntax; nothing in it runs.
Fatal error
An error that stops the script. "Uncaught ..." fatal errors are exceptions nobody caught; the others (memory, redeclare) cannot be caught.
Warning
A reported problem after which the script continues, usually with null in place of a value.
Deprecated
Code that still works but will stop working in a future PHP version.
Stack trace
The list of active calls when an error happened, innermost (#0) first, ending at {main}.
display_errors
php.ini setting: print errors into the output. On in development, Off in production.
error_log
Function and setting for writing messages to the server log, stderr or a file.
Xdebug
The PHP debugging extension: breakpoints, stepping, variable inspection and profiling.
Static analysis
Checking code for errors without running it; PHPStan and Psalm for PHP.
Null-safe operator
?-> — calls a method or reads a property only if the left side is not null; otherwise gives null.
Quick check
A file starts with <?php, line 2 has echo "a"; and line 5 has a syntax error. What is printed?
PHP compiles the whole file before running any of it. A parse error anywhere means the file never starts, so line 2 never runs.
Quick check
The message says headers already sent by (output started at config.php:12). Where is the bug?
The header() call is only where PHP noticed. The cause is the output reported in "output started at" — often whitespace after a closing ?> at the end of an included file.
What is the difference between a warning and a fatal error in PHP?
A warning reports a problem and lets the script continue, usually substituting null for a missing value, so the result may be silently wrong. A fatal error stops the script. Uncaught exceptions are shown as fatal errors but could have been caught; engine fatals such as running out of memory cannot be caught.
How do I fix "Cannot modify header information - headers already sent"?
Look at the "output started at file:line" part of the message: that line sent output before your header(), setcookie() or session_start() call. Remove it — typically whitespace or a blank line before tag. Leave out the closing ?> in PHP-only files.
Should display_errors be on in production?
No. Set display_errors=Off and log_errors=On in production, with error_reporting=E_ALL. Printed errors can leak file paths, SQL and configuration to visitors. Read errors from the log or an error tracker instead.
Finish the PHP handbook, then get hired
Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.