Skip to content

Fix: PHP Warning: Undefined array key (Undefined index)

FixDevs ·

Quick Answer

Fix the PHP warning Undefined array key, Undefined index, and Trying to access array offset on null by checking keys, using the null coalescing operator, and handling PHP 8 strictness.

The Error

You run your PHP script and see one of these warnings:

Warning: Undefined array key "username" in /var/www/html/app.php on line 12
Notice: Undefined index: username in /var/www/html/app.php on line 12
Warning: Trying to access array offset on value of type null in /var/www/html/app.php on line 15

The first two are the same problem. PHP 8.0 upgraded Undefined index from a Notice to a Warning, and renamed it to Undefined array key. The third appears when the variable itself is null rather than an array.

All three mean the same thing: you are reading an array key that does not exist.

$data = ['name' => 'Alice'];
echo $data['email']; // Warning: Undefined array key "email"

The script may still run, but the value returned is null, and your logic silently breaks. In production, these warnings fill your error logs and can leak internal paths to users if display_errors is on.

Why This Happens

PHP arrays are hash maps. When you access a key that was never set, PHP does not throw an exception. It returns null and emits a warning. This is different from languages like Python, which raise a KeyError immediately (see Python KeyError for comparison).

The most common causes:

  1. Accessing superglobals without checking them. You read $_GET['page'] or $_POST['token'] without confirming the key was sent in the request.

  2. Optional or missing data. An API response or database row does not always contain every key you expect.

  3. Typos in key names. You stored the value under user_name but read it as username.

  4. PHP 8.0 upgrade. Code that ran silently on PHP 7.x now emits visible warnings because the severity was raised from E_NOTICE to E_WARNING.

  5. Null variables. The variable you are indexing is not an array at all. It is null, so any key access triggers Trying to access array offset on value of type null.

  6. Conditional data flow. A key is only set inside an if block, but you read it outside that block unconditionally.

Fix 1: Check the Key Exists with isset() or array_key_exists()

The most straightforward fix. Check before you access:

if (isset($data['email'])) {
    echo $data['email'];
}

isset() returns false if the key does not exist or if its value is null. If you need to distinguish between “key missing” and “key is explicitly null,” use array_key_exists():

if (array_key_exists('email', $data)) {
    // Key exists, even if its value is null
    $email = $data['email'];
}

When to use which:

FunctionKey missingKey exists, value is null
isset($arr['k'])falsefalse
array_key_exists('k', $arr)falsetrue

For most cases, isset() is the right choice. It is faster and handles the “key missing or null” scenario in one call. Use array_key_exists() only when null is a meaningful, distinct value in your data.

// Practical pattern: process only provided fields
$allowedFields = ['name', 'email', 'phone'];
foreach ($allowedFields as $field) {
    if (isset($input[$field])) {
        $clean[$field] = htmlspecialchars($input[$field]);
    }
}

Pro Tip: Avoid using empty() as a key-existence check. empty($arr['key']) suppresses the warning but also returns true for 0, "", "0", and false. This hides legitimate values and introduces subtle bugs. Use isset() for existence checks and validate the value separately.

Fix 2: Use the Null Coalescing Operator (??)

PHP 7.0 introduced ??, which returns the left operand if it exists and is not null, otherwise the right operand:

$email = $data['email'] ?? 'not provided';

This is equivalent to:

$email = isset($data['email']) ? $data['email'] : 'not provided';

The ?? operator is clean, readable, and designed specifically for this pattern. You can chain it:

$name = $data['display_name'] ?? $data['username'] ?? $data['email'] ?? 'Anonymous';

You can also use the null coalescing assignment operator (??=) to set a default only if the key is missing:

$config['timeout'] ??= 30; // Sets to 30 only if 'timeout' is not set or is null

This operator works in PHP 7.4 and later.

Fix 3: Fix $_GET, $_POST, and $_SESSION Access

Superglobals are the most common source of this warning. Form fields, query parameters, and session values are not guaranteed to exist.

Bad:

$page = $_GET['page'];
$token = $_POST['csrf_token'];
$user = $_SESSION['user_id'];

Good:

$page = $_GET['page'] ?? 1;
$token = $_POST['csrf_token'] ?? '';
$user = $_SESSION['user_id'] ?? null;

For $_SESSION, always call session_start() before accessing it. If the session has not started, $_SESSION itself is undefined:

if (session_status() === PHP_SESSION_NONE) {
    session_start();
}
$user = $_SESSION['user_id'] ?? null;

For $_GET and $_POST, validate after retrieving:

$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT);
if ($page === null || $page === false) {
    $page = 1;
}

filter_input() returns null if the key does not exist and false if validation fails. It never triggers an undefined key warning.

This same pattern applies to similar warnings in JavaScript when reading object properties that may not exist. If you work across both languages, see TypeError: Cannot read properties of undefined for the JavaScript equivalent.

Fix 4: Fix JSON Decoded Data Access

When you decode JSON, the result may not contain every key you expect:

$json = '{"name": "Alice"}';
$data = json_decode($json, true);
echo $data['email']; // Warning: Undefined array key "email"

Always check json_decode() succeeded and validate keys:

$data = json_decode($json, true);

if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException('Invalid JSON: ' . json_last_error_msg());
}

$email = $data['email'] ?? null;

For complex JSON structures, consider a helper function:

function array_get(array $arr, string $key, mixed $default = null): mixed
{
    return array_key_exists($key, $arr) ? $arr[$key] : $default;
}

$email = array_get($data, 'email', '[email protected]');

If your JSON structure has nested keys, use dot-notation with a recursive lookup:

function array_dot_get(array $arr, string $path, mixed $default = null): mixed
{
    $keys = explode('.', $path);
    foreach ($keys as $key) {
        if (!is_array($arr) || !array_key_exists($key, $arr)) {
            return $default;
        }
        $arr = $arr[$key];
    }
    return $arr;
}

// Usage
$city = array_dot_get($data, 'address.city', 'Unknown');

Laravel users already have this built in via data_get() and Arr::get().

Fix 5: Fix PHP 8.0+ Strictness (Notice to Warning Upgrade)

In PHP 7.x, accessing an undefined array key emitted an E_NOTICE. Many codebases suppressed notices entirely with error_reporting(E_ALL & ~E_NOTICE). After upgrading to PHP 8.0, the same code now triggers E_WARNING, which is harder to suppress and shows up in logs you actually monitor.

Do not fix this by lowering error reporting. The warning exists because silent null returns cause real bugs. Fix the root cause instead.

If you are migrating a large codebase, find all instances systematically:

# Find potential undefined key accesses in PHP files
grep -rn '\$_GET\[' --include="*.php" src/
grep -rn '\$_POST\[' --include="*.php" src/
grep -rn '\$_SESSION\[' --include="*.php" src/
grep -rn '\$_COOKIE\[' --include="*.php" src/

Then wrap each access with ?? or isset(). For a quick audit, enable full error reporting in your development environment:

// In your development config
error_reporting(E_ALL);
ini_set('display_errors', '1');

Run your test suite and fix every warning. This is the cleanest path forward. Suppressing warnings with @ (the error suppression operator) masks bugs and makes debugging harder.

If your PHP project is running into memory issues during these large-scale fixes, check PHP Fatal Error: Allowed Memory Size and Composer Memory Limit for solutions.

Fix 6: Fix Database Result Array Access

Database queries can return null or false when no rows match. Accessing a key on that value triggers the Trying to access array offset on null warning:

$row = $pdo->query("SELECT name FROM users WHERE id = 999")->fetch(PDO::FETCH_ASSOC);
echo $row['name']; // Warning if no row found: $row is false

Always check that the query returned a result:

$stmt = $pdo->prepare("SELECT name, email FROM users WHERE id = :id");
$stmt->execute(['id' => $userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);

if ($row === false) {
    // No matching row
    echo 'User not found';
} else {
    echo $row['name'];
}

For mysqli:

$result = $mysqli->query("SELECT name FROM users WHERE id = 1");
$row = $result->fetch_assoc();

if ($row === null) {
    echo 'No results';
} else {
    echo $row['name'];
}

Note: PDOStatement::fetch() returns false on no results, while mysqli_result::fetch_assoc() returns null. Know which one your driver uses.

When processing multiple rows, the loop naturally handles the empty case:

$stmt = $pdo->query("SELECT name FROM users");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    // $row is always a valid array inside this loop
    echo $row['name'];
}

Fix 7: Use match/switch with a Default for Expected Keys

When you access array keys based on a known set of values, use match (PHP 8.0+) or switch with a default branch:

$statusLabels = [
    'active' => 'Active',
    'inactive' => 'Inactive',
    'banned' => 'Banned',
];

// Bad: triggers warning if $user['status'] is an unexpected value
$label = $statusLabels[$user['status']];

// Good: provide a fallback
$label = $statusLabels[$user['status']] ?? 'Unknown';

With match:

$label = match($user['status'] ?? '') {
    'active' => 'Active',
    'inactive' => 'Inactive',
    'banned' => 'Banned',
    default => 'Unknown',
};

The match expression throws an UnhandledMatchError if no arm matches and there is no default. Always include a default arm when the input comes from external data.

This pattern is especially useful for mapping configuration values, HTTP status codes, or enum-like strings:

$httpMessage = match($code) {
    200 => 'OK',
    301 => 'Moved Permanently',
    404 => 'Not Found',
    500 => 'Internal Server Error',
    default => 'HTTP ' . $code,
};

Common Mistake: Using match without a default arm on user-controlled input. If an unexpected value comes in, PHP throws an UnhandledMatchError exception instead of a warning. This is worse than the original problem. Always add a default when the input is not fully controlled by your code.

Fix 8: Fix Multidimensional Array Access

Nested arrays require checking at every level. A single isset() handles the full path:

$config = [
    'database' => [
        'host' => 'localhost',
    ],
];

// Bad: if 'database' key is missing, this triggers a warning
echo $config['database']['port'];

// Good: isset checks the full path
if (isset($config['database']['port'])) {
    echo $config['database']['port'];
}

// Also good: null coalescing
$port = $config['database']['port'] ?? 3306;

The ?? operator short-circuits. If $config['database'] does not exist, PHP does not attempt to read ['port'] on null. This is safe:

$value = $config['level1']['level2']['level3'] ?? 'default';

However, this only works with ?? and isset(). A direct access chain will trigger warnings at the first missing level:

// This triggers TWO warnings if 'level1' is missing
$value = $config['level1']['level2']['level3'];

For dynamic nested access, use the dot-notation helper from Fix 4, or PHP’s array_reduce:

function nested_get(array $arr, array $keys, mixed $default = null): mixed
{
    foreach ($keys as $key) {
        if (!is_array($arr) || !array_key_exists($key, $arr)) {
            return $default;
        }
        $arr = $arr[$key];
    }
    return $arr;
}

$port = nested_get($config, ['database', 'port'], 3306);

Frameworks like Laravel, Symfony, and CakePHP all provide array helpers for this. Use them instead of rolling your own in framework projects.

This concept of safely accessing nested structures is not unique to PHP. JavaScript developers face the same issue with nested objects, often triggering TypeError: is not a function or similar type errors when chaining calls on undefined values. JavaScript solved this with optional chaining (?.), and PHP’s ?? serves a similar role for arrays.

Still Not Working?

If the warning persists after applying the fixes above, check these less obvious causes:

The variable is not an array at all. Verify the type before accessing keys:

if (!is_array($data)) {
    error_log('Expected array, got ' . gettype($data));
    $data = [];
}

The key is an integer, not a string. PHP arrays distinguish between $arr[0] and $arr['0'] in some edge cases. Check the actual keys:

var_dump(array_keys($data));

A reference or extract() is overwriting your variable. If you use extract() on user input (never do this), it can overwrite local variables. Search your codebase for extract( and remove or replace it.

An error handler is converting warnings to exceptions. Frameworks like Laravel convert all warnings to exceptions via set_error_handler. Your code may be “correct” in vanilla PHP but still throws in the framework. Wrap access with ?? to prevent the warning entirely.

The array comes from a serialized source. unserialize() can produce unexpected structures if the serialized data is corrupted or from an older version of your code. Validate the structure after deserializing:

$data = unserialize($cached, ['allowed_classes' => false]);
if (!is_array($data) || !isset($data['expected_key'])) {
    $data = getDefaultData();
}

You are using a variable variable. Constructs like $$key are unpredictable and hard to debug. Replace them with explicit array access and proper key checks.

If none of these apply, enable xdebug and set a breakpoint on the offending line. Inspect the array contents at runtime to see exactly what keys are present. The root cause is always that the key you expect does not exist in the array at the moment of access. Once you identify why it is missing, the fix follows naturally.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles