Top 10 PHP Bugs Every Beginner Makes (And How to Fix Them)
Fix these once, stop seeing the same crashes forever

You write what looks like perfectly reasonable PHP. You hit refresh. Blank white screen — no error, no warning, no clue what went wrong.
This is PHP's most frustrating feature for beginners: it fails silently by default. Unlike compiled languages that catch mistakes upfront, PHP lets you deploy broken code and gives you nothing in return.
The good news? Every PHP beginner hits the same ten bugs. They're predictable, fixable, and once you understand them, you'll never lose hours to them again.
Let's go through each one.
Before You Start: Enable Error Reporting
This single step changes everything. Without it, half the bugs below are completely invisible.
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
Add this at the very top of your PHP file during local development. PHP hides errors by default in many server configurations — this overrides that behavior and surfaces everything.
Production rule: Set
display_errors = Offin yourphp.iniand write errors to a log file instead. Never expose raw errors to end users.
With error reporting on, most of the bugs below become self-explanatory. Without it, debugging feels like guesswork.
Bug 1: The Blank White Screen (Syntax Error)
The most common beginner panic. You add one line, refresh the browser, and get a completely blank page — not even an HTTP error.
What's happening: PHP hit a syntax error and stopped executing. Since errors are hidden by default, you see nothing.
<?php
$username = "Sanvi" // Missing semicolon — execution stops here
echo "Hello, " . $username;
The fix:
<?php
$username = "Sanvi"; // Every statement ends with a semicolon
echo "Hello, " . $username;
Common culprits: missing semicolons, unclosed brackets, mismatched quotes. With error_reporting(E_ALL) on, PHP points you to the exact line number.
Bug 2: Loose Comparison Traps (== vs ===)
PHP's == operator performs type coercion — it converts values to a common type before comparing. The results are often counterintuitive:
<?php
var_dump("0" == false); // bool(true)
var_dump("" == null); // bool(true)
var_dump(0 == "foo"); // bool(true) in PHP 7 / bool(false) in PHP 8
These aren't bugs in PHP — they're documented behavior. But they catch almost every beginner off guard.
The fix: Default to === for all comparisons. It checks both value and type with zero coercion.
<?php
$userInput = "0";
if ($userInput === false) {
// This won't run — "0" is a string, not a boolean false
}
if (\(userInput === "" || \)userInput === null) {
echo "Actually empty";
}
PHP 8.x tightened some of the worst == behavior, but === is always the safer, more predictable default regardless of PHP version.
Bug 3: Undefined Variable Warnings
Notice: Undefined variable: userName
Your page may still load, but this notice means your logic has already gone off the rails — a variable was used before being assigned, or a typo created a new variable instead of referencing the existing one.
<?php
echo "Welcome, " . $userName; // Never assigned — typo? wrong scope?
The fix:
<?php
\(userName = \)_GET['name'] ?? 'Guest';
echo "Welcome, " . $userName;
The ?? (null coalescing) operator returns the right-hand value when the left side is null or undefined. It's available from PHP 7 onward and is cleaner than wrapping everything in isset().
Bug 4: Variable Scope Inside Functions
PHP uses function-level scope. Variables defined outside a function are completely invisible inside it — they don't exist unless explicitly passed in.
<?php
$siteName = "MyApp";
function showHeader() {
echo "<h1>" . $siteName . "</h1>"; // Notice: Undefined variable
}
showHeader();
The fix: Pass values as parameters.
<?php
$siteName = "MyApp";
function showHeader(string $name): void {
echo "<h1>" . htmlspecialchars($name) . "</h1>";
}
showHeader($siteName);
You can use the global keyword to pull in outer variables, but it creates hidden dependencies and makes functions harder to test. Parameters are the right approach.
Bug 5: SQL Injection from Unparameterized Queries
This crosses from bug into vulnerability. Directly embedding user input into SQL queries lets attackers manipulate your database structure.
<?php
// Never do this:
\(userId = \)_GET['id'];
\(query = "SELECT * FROM users WHERE id = " . \)userId;
// Attacker sends: ?id=1 OR 1=1
// Now the query returns every row in the table
\(result = mysqli_query(\)conn, $query);
The fix: Always use prepared statements.
<?php
\(userId = \)_GET['id'];
\(stmt = \)conn->prepare("SELECT * FROM users WHERE id = ?");
\(stmt->bind_param("i", \)userId); // "i" = integer
$stmt->execute();
\(result = \)stmt->get_result();
The ? placeholder locks the query structure before user input is inserted. MySQL treats the value as data — not as executable SQL. This is non-negotiable for any production code.
Bug 6: Skipping htmlspecialchars() on Output
If SQL injection targets your database, XSS (Cross-Site Scripting) targets your users. Rendering raw user input in HTML lets attackers inject scripts that run in other users' browsers.
<?php
// If $_POST['comment'] contains <script>alert('xss')</script> — it executes
echo "<p>" . $_POST['comment'] . "</p>";
The fix:
<?php
\(safeComment = htmlspecialchars(\)_POST['comment'], ENT_QUOTES, 'UTF-8');
echo "<p>" . $safeComment . "</p>";
htmlspecialchars() converts characters like <, >, and " into their HTML entity equivalents — safe to display, impossible to execute. The ENT_QUOTES flag covers both single and double quotes. The UTF-8 argument prevents multi-byte encoding bypass tricks.
Bug 7: "Headers Already Sent" Error
Warning: Cannot modify header information — headers already sent by (output started at index.php:3)
This error means header() or setcookie() was called after PHP had already flushed output to the browser. Even a single space or blank line before <?php is enough to trigger it.
<?php // ← Space before this tag causes the issue
echo "Loading..."; // Output already sent
header("Location: /dashboard.php"); // Too late — error thrown here
The fix:
<?php
// No output before header() — not even whitespace
$isLoggedIn = checkLogin();
if (!$isLoggedIn) {
header("Location: /login.php");
exit(); // Always exit after redirecting
}
echo "Welcome to the dashboard";
The exit() call after header() is essential. Without it, the rest of your script continues executing even as the browser follows the redirect.
Bug 8: Array Key Access Without Existence Checks
Accessing a form field or array key that might not exist throws a notice and corrupts your conditional logic.
<?php
\(email = \)_POST['email']; // Undefined index if field wasn't submitted
The fix:
<?php
\(email = trim(\)_POST['email'] ?? '');
if ($email !== '') {
// Safe to process
}
?? handles the missing key gracefully. trim() strips accidental leading/trailing whitespace. Both are worth including for any form field access.
Bug 9: Confusing include and require
They look nearly identical but behave very differently when a file is missing:
include |
require |
|
|---|---|---|
| File not found | PHP warning, script continues | Fatal error, script halts |
| Use case | Optional templates, partials | Critical files (DB, config, auth) |
| Recommended variant | include_once |
require_once |
<?php
include 'db_connection.php';
// File missing? PHP continues running...
// ...then crashes on the next database call with a confusing error
The fix:
<?php
require_once 'db_connection.php';
// File missing? PHP stops immediately with a clear, actionable error
Use require_once for anything your script cannot function without. Reserve include for genuinely optional components.
Bug 10: Ignoring Return Values
PHP functions signal failure by returning false. If you don't check, you'll pass false into the next function and get cryptic downstream errors that look nothing like the real problem.
<?php
$fileContents = file_get_contents('/path/to/missing-file.txt');
// Returns false — but we treat it like a string
\(lines = explode("\n", \)fileContents);
// Warning: expects parameter 2 to be string, bool given
The fix:
<?php
$fileContents = file_get_contents('/path/to/config.txt');
if ($fileContents === false) {
throw new RuntimeException("Config file could not be read.");
}
\(lines = explode("\n", \)fileContents); // Safe
Always check for false before using the result of filesystem, database, or network functions. One unchecked failure can cascade into a dozen confusing errors further down.
Summary: 4 Habits That Prevent Most PHP Bugs
After working through this list, a pattern emerges. Most of these bugs share the same root causes:
| Habit | What it prevents |
|---|---|
error_reporting(E_ALL) in development |
Hidden errors, blank screens |
Default to === over == |
Type coercion surprises |
| Check return values before using them | Silent failures cascading downstream |
| Sanitize all user input | SQL injection, XSS vulnerabilities |
These aren't advanced techniques. They're baseline habits — and building them early will save you enormous debugging time as your PHP projects grow.
Your Next Step
Take any PHP project you're working on and audit it against this list. Check for raw \(_POST or \)_GET values going directly into queries or output. Look for include where require_once should be. Check if error reporting is actually on.
You'll probably find at least three of these hiding somewhere.
For the full breakdown with extended code examples: Top 10 PHP Bugs Every Beginner Makes (And How to Fix Them)
Found one of these in your own code? Drop a comment — would love to hear which one catches people most often.





