PHP Null Coalescing Operator (?? and ??=)
The null coalescing operator (??) is a shorthand introduced in PHP 7 that returns its left-hand operand if that value exists and is not null, or its right-hand operand otherwise — all without triggering a warning if the left side is undefined. PHP 7.4 added its sibling, the null coalescing assignment operator (??=), which assigns a value to a variable only when that variable is currently null or unset. Together they replace verbose isset() checks and ternary expressions with a single, readable expression, and they are among the most heavily used features in modern PHP for handling missing array keys, optional arguments, and nullable object properties.
Overview: How the Null Coalescing Operator Works
Before PHP 7, checking for a possibly-missing value meant writing isset($array['key']) ? $array['key'] : 'default' — repeating the expression twice and cluttering the code. The ?? operator collapses this into $array['key'] ?? 'default'. Internally, PHP does not perform an ordinary “read” of the left-hand expression first; the Zend engine compiles ?? to a dedicated COALESCE opcode that performs an isset()-style existence check rather than a value fetch. That is why $undefinedVar ?? 'fallback' never raises an “Undefined variable” warning the way echo $undefinedVar; would — the engine is asking “does this exist and is it non-null?” instead of “give me this value no matter what.”
This existence check works uniformly for plain variables, array elements (including deeply nested ones), object properties, static properties, and the results of function or method calls. If the check succeeds, the left value is used as-is and the right-hand expression is never evaluated at all.
Short-Circuit Evaluation and Chaining
?? short-circuits just like && or ||: the right-hand side only runs if it’s actually needed. This matters whenever the fallback has side effects — for example a fallback that queries a database or writes a log entry will not execute unless the left side is genuinely missing. The operator is also right-associative, which lets you chain several possibilities left to right: $a ?? $b ?? $c ?? 'default' checks each operand in order and returns the first one that exists and isn’t null. In PHP’s operator precedence table, ?? binds tighter than the ternary operator ?: but looser than ||, which is why it usually composes cleanly inside larger expressions without extra parentheses.
The Assignment Form: ??=
The assignment form, $x ??= $y;, is shorthand for $x = $x ?? $y; but with one extra guarantee: the right-hand side is evaluated, and the assignment performed, only if $x is currently null or unset. If $x already holds a non-null value — even 0, false, or an empty string — nothing happens at all: no read of the fallback, no write, no warning. This makes ??= ideal for lazily filling in default configuration values without clobbering anything a caller has deliberately set.
Syntax
| Form | Meaning |
|---|---|
expr1 ?? expr2 |
Returns expr1 if it exists and is not null; otherwise returns expr2. expr2 is evaluated only when needed. |
expr1 ?? expr2 ?? expr3 |
Chained form, evaluated left to right; returns the first operand that exists and is not null. |
$variable ??= expr; |
Assigns expr to $variable only if $variable is currently null or unset. Equivalent to $variable = $variable ?? expr;. |
A minimal example showing both forms together:
<?php
$a = null;
$b = 'fallback';
$result = $a ?? $b;
$arr = [];
$arr['key'] ??= 'default value';
echo $result . ' / ' . $arr['key'];
Output:
fallback / default value
Here $a is null, so $result takes $b‘s value, 'fallback'. The array $arr has no 'key' entry, so ??= creates it with 'default value'.
Examples
Example 1: Default values for missing array keys
<?php
$queryParams = ['page' => '2'];
$sort = $queryParams['sort'] ?? 'newest';
$page = $queryParams['page'] ?? '1';
echo "Sort: $sort, Page: $page";
Output:
Sort: newest, Page: 2
The 'sort' key doesn’t exist in $queryParams, so ?? silently falls back to 'newest' with no warning. The 'page' key does exist, so its actual value, '2', is used.
Example 2: Chaining multiple fallbacks
<?php
function getPreferredTheme(array $user, array $session, array $defaults): string {
return $user['theme'] ?? $session['theme'] ?? $defaults['theme'] ?? 'light';
}
$user = [];
$session = ['theme' => null];
$defaults = ['theme' => 'dark'];
echo getPreferredTheme($user, $session, $defaults);
Output:
dark
$user['theme'] doesn’t exist, so it’s skipped. $session['theme'] exists but is explicitly null, so it counts as “missing” too and is also skipped. $defaults['theme'] is 'dark', the first genuinely non-null value in the chain, so it wins.
Example 3: Filling in defaults with ??=
<?php
function buildRequestOptions(array $options): array {
$options['timeout'] ??= 30;
$options['retries'] ??= 3;
$options['verify_ssl'] ??= true;
return $options;
}
$custom = buildRequestOptions(['timeout' => 5]);
print_r($custom);
Output:
Array
(
[timeout] => 5
[retries] => 3
[verify_ssl] => 1
)
Because 'timeout' was already 5 in the caller-supplied array, ??= leaves it untouched. 'retries' and 'verify_ssl' were missing, so they get filled in with their defaults. print_r renders the boolean true as 1.
Example 4: Combining ?? with the nullsafe operator
<?php
class Profile {
public function __construct(public ?string $bio = null) {}
}
class Account {
public function __construct(public ?Profile $profile = null) {}
}
$account = new Account();
$bio = $account->profile?->bio ?? 'No bio provided.';
echo $bio;
Output:
No bio provided.
$account->profile is null, so the nullsafe operator ?-> short-circuits the whole chain to null instead of raising a warning for reading a property off null. The ?? operator then supplies the fallback string. This pairing — ?-> to traverse an optional object graph, ?? to supply the final default — is one of the most common idioms in modern PHP 8 code.
Under the Hood: Evaluation Step by Step
- PHP evaluates whether the left-hand expression “exists and is not null,” using the same rules as
isset(): for a variable, has it been assigned; for an array key, is it present; for a property, is it declared and initialized. - This check is compiled to a distinct opcode rather than a normal value read, which is why it never emits “Undefined variable,” “Undefined array key,” or “Undefined property” diagnostics the way a direct read would.
- If the check succeeds, that value becomes the result of the expression, and the right-hand side is never evaluated — it is skipped entirely, including any function calls or side effects it contains.
- If the check fails (the value is null or doesn’t exist), PHP evaluates the right-hand expression and that becomes the result.
- For
??=, PHP performs the same left-side check first. If the left side is already non-null, execution stops there: no assignment, no evaluation of the right-hand side, no warning. If it is null or unset, the right-hand side is evaluated and assigned to the left, which must be a writable reference such as a variable, array element, or property. - Chained
??expressions are evaluated strictly left to right, stopping at the first non-null operand — later operands in the chain are never touched once a value is found.
Common Mistakes
Mistake 1: Confusing ?? with the ternary shorthand ?:
A very common bug happens when developers assume ?? treats any “falsy” value — 0, '', false, an empty array — as missing, the way the short ternary ?: does. ?? only checks for null or unset, nothing else.
<?php
$count = 0;
$label = $count ?? 'No items';
echo $label;
Output:
0
The developer likely expected 'No items', but 0 is not null, so ?? passes it straight through. If falsy values should also trigger the fallback, use the short ternary ?: (or an explicit check) instead:
<?php
$count = 0;
$label = $count ?: 'No items';
echo $label;
Output:
No items
Mistake 2: Assuming ?? guards against fatal errors and exceptions
?? only guards against a value being null or undefined — it does nothing to protect a chain of method calls from failing partway through. PHP must fully evaluate the left-hand expression, including any method call inside it, before ?? ever gets a chance to inspect the result.
<?php
class Record {
public function toArray(): array {
return ['id' => 1];
}
}
class Repository {
public function find(int $id): ?Record {
return null;
}
}
$repo = new Repository();
$record = $repo->find(42)->toArray() ?? [];
Output:
Fatal error: Uncaught Error: Call to a member function toArray() on null
find(42) returns null, and calling ->toArray() directly on null is a fatal error — the script dies before ?? can offer its fallback. Fix this by adding the nullsafe operator so the chain short-circuits to null as soon as it hits a null value, before ever reaching ->toArray():
<?php
class Record {
public function toArray(): array {
return ['id' => 1];
}
}
class Repository {
public function find(int $id): ?Record {
return null;
}
}
$repo = new Repository();
$record = $repo->find(42)?->toArray() ?? [];
var_dump($record);
Output:
array(0) {
}
With ?-> in place, the null result from find(42) short-circuits the chain to null instead of crashing, and ?? [] then supplies an empty array as the final fallback.
Best Practices
- Reserve
??for values that are genuinely optional or nullable — missing array keys, optional config, nullable properties — not as a general “treat falsy as missing” tool; use?:or an explicit check for that. - Combine
?->and??when walking optional object graphs, so a null in the middle of a chain produces a clean fallback instead of a fatal error. - Use
??=to populate default array or config values without ever overwriting a value that was legitimately set to0,'', orfalse. - Don’t lean on long
??chains as a substitute for real validation; if a value is genuinely required, validate it and throw a clear exception rather than silently substituting a default that might mask a bug. - Remember that
??suppresses undefined-key/undefined-property notices entirely — pair it with static analysis tools (Psalm, PHPStan) or tests so a typo’d array key doesn’t silently fall back to a default forever. - Prefer
??and??=overisset() ? ... : ...for readability, but only where the left side is a simple variable, array offset, or property access thatisset()can actually check.
Practice Exercises
- Write a function
getConfigValue(array $config, string $key, mixed $default): mixedthat returns$config[$key]if it exists and is not null, otherwise returns$default, using??. - Given a session array that may or may not contain
'user_id', write a single line using??that assigns the value to$userId, defaulting to0when it’s missing. - Using
??=, write a snippet that ensures an$optionsarray has both'width'and'height'keys, defaulting each to100, without overwriting any value already present — including a value of0that was deliberately set.
Summary
??returns the left operand if it exists and is notnull, otherwise the right operand, without warnings for undefined variables, keys, or properties.??=assigns the right-hand value to the left only when the left isnullor unset; it’s shorthand for$x = $x ?? $y;.??only cares aboutnull— unlike?:, which treats any falsy value as “missing,” values like0,'',false, and[]pass straight through??.??is right-associative and short-circuits, soa ?? b ?? c ?? dchains cleanly and never evaluates operands past the first non-null one.??does not catch exceptions or fatal errors raised while producing the left-hand value; combine it with the nullsafe operator?->for safe optional chaining.- Reserve
??for values that are genuinely allowed to be missing, and keep explicit validation for values your code truly requires.
