skip to content

In PHP, how do the ??, ?:, ??= and ?-> operators differ, and when does each one fall back?

level: middleimportance: must knowfreq 70%

answer

  1. null versus falsy
  2. ?? behaves like isset, no warning
  3. ?: keeps "0" out, ?? keeps it
  4. ??= since 7.4, ?-> since 8.0
  5. ?-> skips the rest of the chain

basics

~20 s

?? falls back when the left side is null or undefined, without a warning; ?: falls back on any falsy value; ??= assigns only when null or unset; ?-> returns null for a null object and skips the rest of the chain.

solid answer

~40 s

`$a ?? $b` yields `$a` unless it is `null` or does not exist, and like `isset()` it raises no warning for a missing variable, key or property. `$a ?: $b` (the short ternary) yields `$a` unless it is **falsy**, so `0`, `"0"`, `""` and `[]` fall back too, and an undefined `$a` still warns. `$x ??= $default` (PHP 7.4) assigns only when `$x` is `null` or unset, and evaluates the right side only then. `$obj?->method()` (PHP 8.0) returns `null` when `$obj` is `null` and skips the rest of the chain; it does not silence an undefined property on a real object, and it cannot be used as an assignment target. One precedence trap: `??` binds more loosely than `.`, so `'Hi ' . $name ?? 'guest'` needs parentheses.

code

php · 10 lines
php
<?php
$query = ['page' => '0'];

var_dump($query['page'] ?? '1');   // string(1) "0": not null, kept
var_dump($query['page'] ?: '1');   // string(1) "1": "0" is falsy
var_dump($query['sort'] ?? 'asc'); // string(3) "asc", no warning

$query['limit'] ??= 20;            // assigned: key was missing
$query['page'] ??= '1';            // not assigned: "0" is not null
var_dump($query['limit'], $query['page']); // int(20) string(1) "0"

go deeper

for a junior

Remember that ?? checks for null or missing and never warns, while ?: checks for falsy, so "0" survives ?? but not ?:.

for a middle

Explain isset semantics, lazy evaluation in ??=, the short-circuiting of ?-> and its write-context ban, and the low precedence of ?? next to concatenation.

for a senior

Use these operators to state intent: distinguish "absent" from "empty" in request handling, and prefer an exception over ?-> when null signals a real error.

for a principal

Set team guidance on nullable design: ?-> chains are a sign of optional relationships, and deep chains often mean a missing domain object or null object.

## Four operators, three different tests These operators look alike and are often mixed up in interviews. They differ in **what triggers the fallback** and in **whether a missing variable warns**. | Operator | Since | Falls back when the left side is | Undefined left side | |---|---|---|---| | `$a ?? $b` (null coalescing) | 7.0 | `null` or not set | no warning (isset semantics) | | `$a ?: $b` (short ternary) | before 7.0 | falsy: `false`, `0`, `0.0`, `""`, `"0"`, `[]`, `null` | warning | | `$a ??= $b` (null coalescing assignment) | 7.4 | `null` or not set; then assigns `$b` to `$a` | no warning | | `$o?->p` / `$o?->m()` (nullsafe) | 8.0 | `$o` is `null`; the result is `null` | warning for an undefined `$o` | ## ?? versus ?: The classic question is which one to use for a default. Think of the input `"0"`, a legitimate quantity or page number: - `$_GET['page'] ?? '1'` keeps `"0"`, because `"0"` is not `null`. - `$_GET['page'] ?: '1'` replaces `"0"` with `'1'`, because `"0"` is falsy. If the key is missing, it also warns about an undefined array key. So `??` answers "was a value provided?" and `?:` answers "is the value truthy?". The manual describes `$a ?? $b` as equivalent to `isset($a) ? $a : $b`, which is why it inspects missing keys and properties without a warning. Both evaluate their left side once and chain naturally: `$a ?? $b ?? 'x'` returns the first non-null value, and `0 ?: 0 ?: 3` returns `3`. ## ??= for lazy defaults `$config['timeout'] ??= 30;` writes `30` only when the key is missing or `null`, and leaves an existing `0` alone. The right side is evaluated **only** when the assignment happens, so `$cache[$key] ??= loadExpensive($key);` calls the loader once per key. It is shorter than an `if (!isset(...))` block and safer than `$x = $x ?: 30`, which would overwrite a stored `0`. ## ?-> for optional objects The nullsafe operator guards **one thing**: the object on its left being `null`. The manual gives the equivalence: ```php $result = $repository?->getUser(5)?->name; // same as: null if $repository is null, else null if getUser(5) // returns null, else the user's name ``` When it short-circuits, the **rest of the chain is skipped**, so `$order?->customer()->email` returns `null` without calling `email` on nothing. What it does *not* do: 1. It does not suppress an **undefined property** or **undefined method** on a real object; those still warn or throw. 2. It does not suppress an **undefined variable**: `$missing?->x` warns, although `$missing?->x ?? 'none'` does not, because `??` puts the whole expression in isset mode. 3. It cannot appear in a **write context**. `$user?->name = 'x';` and `$user?->name ??= 'x';` fail to compile with "Can't use nullsafe operator in write context", and taking a reference of a nullsafe chain is also a compile error. The manual's advice is to use `?->` when `null` is an expected, valid value, and to throw an exception when `null` signals an error. ## Precedence traps `??` has **low precedence**: it binds more loosely than arithmetic, concatenation and comparison. The manual's own example: - `print 'Mr. ' . $name ?? 'Anonymous';` parses as `('Mr. ' . $name) ?? 'Anonymous'`, warns that `$name` is undefined and prints `Mr. `. - `print 'Mr. ' . ($name ?? 'Anonymous');` prints `Mr. Anonymous`. Mixing `??` with the full ternary or with `?:` is also a place to add parentheses, because PHP 8 refuses some unparenthesised ternary combinations at compile time. ## Choosing between them - A request parameter or config key that may be absent: `??`. - A value where empty and zero should also be replaced: `?:`, with the variable known to exist. - A default written into an array or property once: `??=`. - A chain through objects that may legitimately be missing: `?->`, often finished with `??` to supply a non-null default.

  • Why does $user?->name = 'Ann'; not compile in PHP 8?
    The nullsafe operator only works for reads. If `$user` were `null` there would be nothing to assign to, and PHP refuses to silently drop a write, so the compiler rejects it with "Can't use nullsafe operator in write context". The same applies to `??=`, `++` and taking a reference through a nullsafe chain. Check for `null` explicitly before writing.
  • Does ?-> silence an undefined property on a non-null object?
    No. It only short-circuits when the object on its left is `null`. If the object exists but lacks the property, PHP still emits the "Undefined property" warning, and an undefined method still throws `Error`. Only wrapping the chain in `??` or `isset()` puts the property read into silent isset mode.
  • What does 'Total: ' . $sum ?? 0 print when $sum is undefined?
    Concatenation binds tighter than `??`, so PHP evaluates `'Total: ' . $sum` first. That warns about the undefined variable and produces the non-null string `'Total: '`, so the `?? 0` fallback never applies. Write `'Total: ' . ($sum ?? 0)` to get `Total: 0`.

saying these in an interview costs you the question

  • Says ?? falls back on any falsy value such as 0 or an empty string.
  • Says ?: suppresses the undefined-variable warning like isset().
  • Thinks ?-> also silences undefined properties on real objects.
  • Tries to assign through ?-> as a null-safe write.
  • Believes ??= always evaluates its right-hand side.