PHP Nullsafe Operator (?->)
The nullsafe operator, written ?->, was introduced in PHP 8.0 to solve one of the most common sources of fatal errors in object-oriented code: calling a method or reading a property on something that turns out to be null. Instead of wrapping every access in isset() or is_null() checks, you replace the ordinary -> arrow with ?->, and PHP automatically short-circuits the whole expression to null the moment it encounters a null value. It is one of the most-used quality-of-life features in modern PHP, especially with nested objects like ORM entities, API response wrappers, and optional configuration values.
Overview: How the Nullsafe Operator Works
Before PHP 8.0, an expression like $user->getAddress()->getCity() would throw a fatal error, “Call to a member function getCity() on null”, the instant getAddress() returned null. Developers worked around this with temporary variables and defensive if checks, or by writing small helper functions that walked an object graph and bailed out early. This was verbose and easy to forget in one spot, which is exactly where bugs hide.
The nullsafe operator moves that defensive check into the language itself. expression?->property first evaluates expression. If the result is null, the entire access is skipped and the whole expression evaluates to null — no property fetch happens, no method is called, and no warning or error is raised. If the result is not null, execution proceeds exactly as it would with a normal ->.
The important part is that this short-circuiting applies to the entire chain, not just a single hop. In $a?->b?->c?->d(), if $a is null, PHP never attempts to read ->b, ->c, or call ->d() at all — the whole expression simply becomes null in one step. This matters because it means no partial side effects occur partway through a chain; either the full chain runs, or none of it does.
The nullsafe operator works for property access and method calls. It does not apply to static access (::) or to array access ([]) directly, though you can mix normal array access into a chain after a nullsafe hop, such as $config?->options['timeout'] — here $config is nullsafe-checked, and if it exists, ordinary array indexing follows. The overall type of a nullsafe expression is always nullable: if any link could be null, static analyzers and your own code should treat the whole expression as possibly null.
Syntax
expression?->property
expression?->method(arg1, arg2)
expression?->prop?->nested?->method()
| Part | Meaning |
|---|---|
expression |
Any expression that evaluates to an object or to null (a variable, a method call, a property access, etc.) |
?-> |
The nullsafe operator. Behaves like -> when the left side is not null; short-circuits the remaining chain to null when it is |
property |
A property name to read from the object |
method(args) |
A method call on the object, with normal argument syntax |
You can freely mix ?-> and -> in the same chain, but every link that might legitimately be null needs its own ?->. A plain -> later in the chain will still fatal if that particular value happens to be null.
Examples
Example 1: Safe property access
<?php
class Address
{
public function __construct(public readonly string $city) {}
}
class User
{
public function __construct(public readonly ?Address $address = null) {}
}
$user1 = new User(new Address("Berlin"));
$user2 = new User();
echo $user1->address?->city . "\n";
echo ($user2->address?->city ?? "No city") . "\n";
Output:
Berlin
No city
$user1 has an Address, so ?->city behaves exactly like a normal ->city and returns “Berlin”. $user2 has no address, so $user2->address is null, and ?->city short-circuits to null instead of fataling. Combining it with ?? supplies a readable fallback.
Example 2: Chained method calls
<?php
class Engine
{
public function horsepower(): int
{
return 450;
}
}
class Car
{
public function __construct(private ?Engine $engine = null) {}
public function getEngine(): ?Engine
{
return $this->engine;
}
}
$car1 = new Car(new Engine());
$car2 = new Car();
echo $car1->getEngine()?->horsepower() . "\n";
var_dump($car2->getEngine()?->horsepower());
Output:
450
NULL
getEngine() is called normally in both cases (it is not the nullable part), but its return value is what the nullsafe operator protects. For $car2, getEngine() returns null, so ?->horsepower() is never actually invoked; the whole expression simply evaluates to null, which var_dump() prints as NULL.
Example 3: Nullsafe chains inside a loop, with defaults
<?php
class Profile
{
public function __construct(private ?string $avatarUrl = null) {}
public function getAvatarUrl(): ?string
{
return $this->avatarUrl;
}
}
class Account
{
public function __construct(private ?Profile $profile = null) {}
public function getProfile(): ?Profile
{
return $this->profile;
}
}
$accounts = [
new Account(new Profile("https://example.com/a.png")),
new Account(new Profile()),
new Account(),
];
foreach ($accounts as $index => $account) {
$url = $account->getProfile()?->getAvatarUrl() ?? "default-avatar.png";
echo "Account {$index}: {$url}\n";
}
Output:
Account 0: https://example.com/a.png
Account 1: default-avatar.png
Account 2: default-avatar.png
Each account short-circuits at a different point in the chain: account 0 has both a profile and an avatar URL, account 1 has a profile but no avatar URL (so getAvatarUrl() returns null), and account 2 has no profile at all (so getProfile()?->getAvatarUrl() short-circuits before getAvatarUrl() is ever called). The ?? operator catches all three null outcomes with one shared default.
Under the Hood: How PHP Evaluates a Nullsafe Chain
Internally, the Zend engine does not treat ?-> as a single isolated operator applied hop by hop. When the compiler parses an expression containing one or more ?-> operators, it recognizes the entire chain of ->/?-> accesses as one compound expression. It compiles this into code that evaluates the base value once, performs a null check, and if the value is null, jumps directly past all the remaining property fetches and method calls in that chain to produce a NULL zval — without emitting any warning and without executing any of the skipped calls.
This has two practical consequences worth understanding. First, performance when the value is not null is essentially identical to a plain -> chain; the only overhead is a cheap null test at each nullsafe hop, which is far cheaper than throwing and catching an exception. Second, because the jump skips the rest of the chain entirely, any method calls further down the chain simply never execute when an earlier link is null — there is no partial execution, and no side effects from those skipped calls occur. This is different from writing several separate if statements with early returns, where you must be careful to write the short-circuiting logic yourself; the nullsafe operator guarantees it as part of the language.
Common Mistakes
Mistake 1: Using ?-> to assign a value
The nullsafe operator can only be used for reading values, not writing them. Using it on the left-hand side of an assignment is a compile-time error, not a runtime one:
<?php
class User
{
public ?Address $address = null;
}
$user = new User();
// Fatal error: Can't use nullsafe operator in write context
$user?->address = new Address("Paris");
PHP rejects this before the script even runs, because a nullsafe operator implies “maybe do nothing”, which makes no sense as the target of an assignment. If $user could genuinely be null here, assigning to one of its properties would fail anyway with a much more fundamental error. The fix is to assign through a plain arrow (guarding the object itself with an if when necessary), and only use ?-> when you read the value back:
<?php
class Address
{
public function __construct(public string $city) {}
}
class User
{
public ?Address $address = null;
}
$user = new User();
// Correct: assign directly, since $user itself is known not to be null here
$user->address = new Address("Paris");
echo $user->address?->city . "\n";
Output:
Paris
Mistake 2: Assuming ?-> protects against undefined members
The nullsafe operator only guards against the base value being null. If the object exists but you call a method that doesn’t exist on it (a typo, or a method you forgot to implement), you still get a fatal error, because that failure has nothing to do with nullability:
<?php
class Invoice
{
public function __construct(private float $total) {}
public function getTotal(): float
{
return $this->total;
}
}
$invoice = new Invoice(199.99);
// $invoice is not null, but getFormattedTotal() does not exist on Invoice.
echo $invoice?->getFormattedTotal();
Output:
Fatal error: Uncaught Error: Call to undefined method Invoice::getFormattedTotal()
The fix is simply to call a method that actually exists, or to implement the missing one:
<?php
class Invoice
{
public function __construct(private float $total) {}
public function getFormattedTotal(): string
{
return number_format($this->total, 2);
}
}
$invoice = new Invoice(199.99);
echo $invoice?->getFormattedTotal() . "\n";
Output:
199.99
Best Practices
- Reserve
?->for values that are genuinely, legitimately optional in your domain — not as a blanket way to silence “could be null” warnings from a static analyzer. - Pair
?->with??whenever the caller needs a concrete value, so a missing link resolves to a sensible default instead of an unexplainednullflowing further into your program. - Avoid very deep nullsafe chains like
$a?->b?->c?->d?->e. If you find yourself writing one, it is often a sign the objects involved should expose a single accessor method (or that a Null Object pattern would simplify the design). - Type-hint properties and return types as nullable (
?Type) wherevernullis a real possibility, so both readers and static analysis tools like PHPStan or Psalm can tell you exactly where?->is required. - Remember that
?->cannot appear in a write context and does not apply to static (::) access; use ordinary conditionals for those cases. - Don’t let
?->hide bugs. If a value should never actually benullat a given point in your program, prefer an assertion or a thrown exception over quietly swallowing the null with a nullsafe chain.
Practice Exercises
Exercise 1: Model three classes, Order, Customer, and Address, where an Order has a nullable Customer and a Customer has a nullable Address. Write a single expression using the nullsafe operator that prints the order’s city, falling back to “Unknown city” when any link in the chain is missing.
Exercise 2: Write a function findUserById(int $id): ?User that returns null for unknown IDs. Using only ?-> and ??, build a one-line greeting such as “Hello, Alice” for a found user, or “Hello, guest” when the user is not found.
Exercise 3: Take a snippet of code that checks for null at every step with nested isset() calls before accessing $shipment->package->dimensions->weight, and rewrite it as a single nullsafe expression combined with ?? for a default weight of 0. Confirm that the behavior is identical to the original nested checks.
Summary
- The nullsafe operator
?->, added in PHP 8.0, safely accesses a property or calls a method when the base value might benull. - If the left-hand side is
null, the operator short-circuits the entire remaining chain tonullwithout evaluating anything further and without raising an error. - It can be freely mixed with normal
->, but any link that might benullneeds its own?->. - It cannot be used on the left-hand side of an assignment, and it does not apply to static (
::) or array ([]) access directly. - It only guards against
null; calling an undefined method or property on a non-null object still causes a fatal error. - Combine it with the null coalescing operator
??to turn an optional chain into a concrete default value. - Internally, PHP compiles a whole nullsafe chain into one short-circuiting jump, so performance when values are present is essentially the same as a normal chain.
