Free Handbook · Every example compiled & verified

Errors & Debugging

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.

0 / 148 lessons🔥 0 day streak
ShareXLinkedIn

Module 11 · what you'll be able to do

  • 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).

Message starts withWhat it isDoes the script continue?Can you catch it?
Parse error: syntax errorThe file is not valid PHP (found when compiling)Nothing runs at allNo — fix the code
Fatal error: Uncaught Error, TypeError, ValueError…A thrown Error nobody caughtStopsYes, catch (Error $e) / Throwable
Fatal error: Uncaught SomethingExceptionA thrown Exception nobody caughtStopsYes, catch (SomethingException $e)
Fatal error: (no "Uncaught")An engine-level failure: out of memory, time limit, redeclared functionStopsNo
Warning:Something went wrong, PHP guessed and carried onYesOnly by converting it (ErrorException)
Notice: / Deprecated:Minor issue / will stop working in a future PHPYesOnly 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:

phpmain.php
<?php
$problems = [];
set_error_handler(function (int $level, string $msg, string $file, int $line) use (&$problems): bool {
    $name = match ($level) {
        E_WARNING => 'Warning',
        E_NOTICE => 'Notice',
        E_DEPRECATED => 'Deprecated',
        default => "Level $level",
    };
    $problems[] = "$name: $msg (line $line)";
    return true;                        // true = handled, do not print it
});

$cart = ['pen' => 20];
$total = $cart['pen'] + $cart['ink'];   // missing key
$name = strtoupper($undefinedName);     // missing variable
$price = trim(null);                    // null to a string parameter

echo "total = $total, name = '$name'\n";
echo count($problems), " problems:\n", implode("\n", $problems), "\n";
Outputcompiled & run with real PHP
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
<?php
declare(strict_types=1);

function loadPrice(array $product): float
{
    return $product['price'];              // line 6: price is a string here
}

function cartTotal(array $cart): float
{
    $sum = 0.0;
    foreach ($cart as $product) {
        $sum += loadPrice($product);       // line 13
    }
    return $sum;
}

try {
    echo cartTotal([['price' => 9.5], ['price' => '12.00']]);   // line 19
} catch (TypeError $e) {
    echo get_class($e), ": ", $e->getMessage(), "\n";
    echo "thrown at line ", $e->getLine(), "\n";
    echo str_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. 1
    Read the first line

    Type and message: TypeError, "return value must be float, string returned". That is what went wrong.

  2. 2
    Go to the reported line

    Line 6, the return in loadPrice(). That is where PHP noticed.

  3. 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. 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
<?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
<?php
$total = 250;
echo "Total: " . $total;     // or: echo "Total: $total";
Error you will hit

2. Warning: Undefined variable

php
<?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
<?php
$items = ['pen', 'ink', 'paper'];
$count = 0;
foreach ($items as $item) {
    $count++;
}
echo "Items: $count\n";   // or simply count($items)
Error you will hit

3. Error: Call to undefined function

php
<?php
$words = ['php', 'is', 'fun'];
echo str_join(' ', $words);
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
<?php
$words = ['php', 'is', 'fun'];
echo implode(' ', $words);   // php is fun
Error you will hit

4. Fatal error: Cannot redeclare function

php
<?php
function formatPrice(float $p): string
{
    return number_format($p, 2);
}

echo formatPrice(9.5), "\n";

function formatPrice(float $p): string
{
    return '$' . number_format($p, 2);
}
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.

php
<?php
function formatPrice(float $p): string
{
    return number_format($p, 2);
}

function formatPriceWithSymbol(float $p): string
{
    return '$' . formatPrice($p);
}

echo formatPriceWithSymbol(9.5), "\n";   // $9.50
Error you will hit

5. Error: Class not found (the namespace trap)

php
<?php
namespace App\Controllers;

$date = new DateTimeImmutable('2026-01-01');
echo $date->format('Y');
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.

php
<?php
namespace App\Controllers;

use DateTimeImmutable;

$date = new DateTimeImmutable('2026-01-01');
echo $date->format('Y');   // 2026
04

Array and null errors

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.

Error you will hit

6. Warning: Undefined array key "phone"

php
<?php
$user = ['name' => 'Asha', 'email' => '[email protected]'];
echo "Phone: " . $user['phone'] . "\n";
echo "Done\n";
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.

php
<?php
$user = ['name' => 'Asha', 'email' => '[email protected]'];
echo "Phone: " . ($user['phone'] ?? 'not given') . "\n";
echo "Done\n";
Error you will hit

7. Warning: Undefined array key 3 — the off-by-one loop

php
<?php
$scores = [90, 72, 85];
for ($i = 0; $i <= count($scores); $i++) {
    echo "Score $i: {$scores[$i]}\n";
}
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
<?php
$scores = [90, 72, 85];
foreach ($scores as $i => $score) {
    echo "Score $i: $score\n";
}
Error you will hit

8. Warning: Trying to access array offset on null

php
<?php
function findUser(int $id): ?array
{
    $users = [1 => ['name' => 'Asha']];
    return $users[$id] ?? null;
}

$user = findUser(2);
echo "Hello, " . $user['name'] . "\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'.

php
<?php
function findUser(int $id): ?array
{
    $users = [1 => ['name' => 'Asha']];
    return $users[$id] ?? null;
}

$user = findUser(2);
if ($user === null) {
    echo "No such user\n";
} else {
    echo "Hello, " . $user['name'] . "\n";
}
Error you will hit

9. Error: Call to a member function on null

php
<?php
class Order
{
    public function __construct(public ?Customer $customer = null) {}
}
class Customer
{
    public function getEmail(): string { return '[email protected]'; }
}

$order = new Order();
echo $order->customer->getEmail();
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.

php
<?php
class Order
{
    public function __construct(public ?Customer $customer = null) {}
}
class Customer
{
    public function getEmail(): string { return '[email protected]'; }
}

$order = new Order();
echo $order->customer?->getEmail() ?? 'guest checkout';
05

Type, argument and value errors

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

php
<?php
declare(strict_types=1);

function applyDiscount(float $price, int $percent): float
{
    return $price * (100 - $percent) / 100;
}

$percent = $_GET['discount'] ?? '10';
echo applyDiscount(200.0, $percent);
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.

php
<?php
declare(strict_types=1);

function applyDiscount(float $price, int $percent): float
{
    return $price * (100 - $percent) / 100;
}

$percent = filter_var($_GET['discount'] ?? '10', FILTER_VALIDATE_INT) ?: 0;
echo applyDiscount(200.0, $percent);   // 180
Error you will hit

11. ArgumentCountError: Too few arguments to function

php
<?php
function sendEmail(string $to, string $subject, string $body): string
{
    return "To: $to | $subject";
}

echo sendEmail('[email protected]', 'Welcome');
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.

php
<?php
function sendEmail(string $to, string $subject, string $body = ''): string
{
    return "To: $to | $subject";
}

echo sendEmail('[email protected]', 'Welcome');
Error you will hit

12. DivisionByZeroError: Division by zero

php
<?php
$orders = [];
$revenue = 0;
$average = $revenue / count($orders);
echo "Average order: $average\n";
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.

The fix

Handle the empty case before dividing.

php
<?php
$orders = [];
$revenue = 0;
$average = count($orders) > 0 ? $revenue / count($orders) : 0;
echo "Average order: $average\n";
Error you will hit

13. ValueError: array_combine() must have the same number of elements

php
<?php
$header = ['id', 'name', 'email'];
$row = ['7', 'Asha'];
$record = array_combine($header, $row);
print_r($record);
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.

php
<?php
$header = ['id', 'name', 'email'];
$row = ['7', 'Asha'];
if (count($row) !== count($header)) {
    echo "Skipping row with ", count($row), " columns, expected ", count($header), "\n";
} else {
    print_r(array_combine($header, $row));
}
06

Web and resource errors

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

php

<?php
session_start();
$_SESSION['user_id'] = 7;
header('Location: /dashboard');
exit;
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).

php
<?php
session_start();
$_SESSION['user_id'] = 7;
header('Location: /dashboard');
exit;
Error you will hit

15. Fatal error: Allowed memory size exhausted

php
<?php
ini_set('memory_limit', '16M');

$rows = [];
for ($i = 0; $i < 1_000_000; $i++) {
    $rows[] = ['id' => $i, 'name' => "user$i", 'email' => "[email protected]"];
}
echo count($rows);
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
<?php
ini_set('memory_limit', '16M');

function users(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)KindUsual causeUsual fix
1Parse error: syntax error, unexpected ...ParseMissing ;, ., ) or }Look at the reported line and the one before
2Warning: Undefined variable $xWarningTypo or variable never initialisedInitialise before use
3Call to undefined functionErrorWrong name, missing include or extensionReal name, require, php -m
4Cannot redeclare functionFatalSame function twice, file included twicerequire_once, namespaces
5Class "..." not foundErrorNamespace lookup, autoload mappinguse or a leading \
6Undefined array key "name"WarningOptional or missing field??, isset()
7Undefined array key 3WarningOff-by-one loop (<=)< or foreach
8Trying to access array offset on nullWarningA lookup returned nullCheck for null first
9Call to a member function ... on nullErrorObject missing (no DB row, optional relation)?->, null check
10TypeError: must be of type int, string givenErrorRequest input passed straight throughValidate and cast at the edge
11ArgumentCountError: Too few argumentsErrorCaller not updated after a signature changePass it or add a default
12DivisionByZeroErrorErrorAverage of an empty listGuard the zero case
13ValueErrorErrorRight type, impossible valueValidate the data first
14Cannot modify header information - headers already sentWarningOutput before header() / session_start()Remove the stray output; no closing ?>
15Allowed memory size ... exhaustedFatalLoading everything into one arrayStream 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
<?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);
echo file_get_contents($log);
unlink($log);
Outputcompiled & run with real PHP
bool(true)
string(5) "19.90"
Array
(
    [id] => 7
    [paid] => 1
    [total] => 19.90
    [items] => Array
        (
            [0] => pen
            [1] =>
        )

)

array (
  'id' => 7,
  'paid' => true,
  'total' => '19.90',
  'items' =>
  array (
    0 => 'pen',
    1 => NULL,
  ),
)
{"id":7,"paid":true,"total":"19.90","items":["pen",null]}
order 7 total=19.90
order 7 items=["pen",null]

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 settingDevelopmentProductionWhy
error_reportingE_ALLE_ALLReport everything; decide separately where it goes
display_errorsOnOffError text in a page leaks paths, SQL and secrets to visitors
log_errorsOnOnThe log is how you find out about errors in production
error_logstderr / a local filea file or syslogWhere logged errors go
memory_limit128M+as neededPer-request cap (error 15)
max_execution_time0 in CLI30 for webStops runaway requests
bash
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
pecl install xdebug             # or: apt install php-xdebug / brew + pecl
php -v                           # should now say "with Xdebug v3..."
iniconf.d/99-xdebug.ini
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
json.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    { "name": "Listen for Xdebug", "type": "php", "request": "launch", "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.

bash
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level 6

#  ------ ------------------------------------------------------------
#   Line   src/Checkout.php
#  ------ ------------------------------------------------------------
#   14     Cannot call method getEmail() on Customer|null.
#   22     Parameter #2 $percent of function applyDiscount expects int,
#          string given.
#  ------ ------------------------------------------------------------

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?

Quick check

The message says headers already sent by (output started at config.php:12). Where is the bug?

Frequently asked questions

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.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.