“Typed property must not be accessed before initialization” means your code read a typed property that has never been assigned a value. Since PHP 7.4, a property with a type and no default does not start as null; it starts in a separate uninitialized state, and reading it throws an Error. The fix is to make sure a value is assigned before the read: a default in the declaration, an assignment in the constructor, or a guard like isset() or ?? where “not set yet” is a legitimate state. The harder part is working out why the assignment you thought happened did not.
The examples below run on PHP 8.1 and later (for readonly) and on PHP 7.4 and later otherwise.
Typed Property Must Not Be Accessed Before Initialization: Reading the Message
The full message names the class and the property:
Fatal error: Uncaught Error: Typed property Shape::$numberOfSides must not be accessed before initialization
That example comes straight from the PHP manual page on properties, which we read on 2026-09-25. The manual’s rule is short: “Typed properties must be initialized before accessing, otherwise an Error is thrown.”
Two things in the message matter. The class name is where the property is declared, which may be a parent class rather than the object you are working with. And the line number is where the read happened, not where the missing assignment should have been. As with most PHP errors, the error line is not where the bug is: the bug is the code path that skipped the assignment.
Uninitialized Is Not Null
This is the misunderstanding behind most occurrences. An untyped property with no default behaves as null. A typed one does not, and making the type nullable does not change that:
<?php
class Order
{
public $note; // untyped: reads as null
public ?string $coupon; // nullable type, no default: UNINITIALIZED
public ?string $ref = null; // nullable with default: null
}
$order = new Order();
var_dump($order->note); // NULL
var_dump($order->ref); // NULL
var_dump($order->coupon); // Error: must not be accessed before initialization
?string means “this property may hold null.” It does not mean “this property starts as null.” Only = null does that. If you are migrating older code and adding types to existing properties, this is the change that turns a working class into a crashing one.
The Five Usual Causes, and How to Tell Which You Have
Before changing code, find out which situation you are in. The stack trace shows the method that did the read; read it from the top down and ask which code path should have assigned the property. If stack traces are unfamiliar, start with how to read a stack trace you did not write.
1. The property is only set in a setter
The manual’s own example is this case. The class declares int $numberOfSides, sets it in setNumberOfSides(), and a caller creates an object and never calls the setter. The getter then throws.
Fix: if the object is not valid without the value, require it in the constructor. If it is optional, give it a default or make the getter handle the missing case.
<?php
class Shape
{
public function __construct(
public string $name,
public int $numberOfSides,
) {}
}
Constructor promotion (PHP 8.0 and later) guarantees both properties are assigned the moment the object exists.
2. A parent constructor calls a method before the child has assigned
This one is subtle and shows up in frameworks with boot() or init() hooks:
<?php
class Base
{
public function __construct()
{
$this->init();
}
protected function init(): void {}
}
class Child extends Base
{
private string $prefix;
public function __construct()
{
parent::__construct(); // init() runs here
$this->prefix = 'app_'; // too late
}
protected function init(): void
{
echo $this->prefix; // Error
}
}
new Child();
Fix: assign the child’s properties before calling parent::__construct(). PHP allows statements before the parent call.
3. The object was created without its constructor
ORMs, serializers, dependency injection containers and unserialize() often create objects without running the constructor, then fill in the properties they know about. A property you assign only in the constructor, and that is not in the stored data, stays uninitialized. Doctrine entity IDs before the first save and cached objects deserialized after you added a new property are the classic examples.
Fix: give such properties a declaration default, or initialize them in __unserialize() or the hook your ORM provides. Do not assume the constructor ran.
4. Something called unset() on it
unset($this->items) does not set a typed property to null. It returns it to the uninitialized state, and the next read throws. The manual’s page on ReflectionProperty::isInitialized() confirms it returns false “for typed properties prior to initialization, and for properties that have been explicitly unset().”
Fix: assign an empty value instead ($this->items = [];), or null if the type is nullable.
5. A readonly property never got its one assignment
A readonly property cannot have a default value in its declaration, so it always starts uninitialized and must be assigned exactly once from inside the class. If one constructor branch returns early, or a named constructor forgets it, you get this error on first read.
<?php
final class Money
{
public readonly int $cents;
public static function fromString(string $amount): self
{
$m = new self();
if ($amount === '') {
return $m; // $cents never assigned
}
$m->cents = (int) round((float) $amount * 100);
return $m;
}
}
Fix: make every path assign it, or throw when the input cannot produce a valid value. A readonly object that is half built is worse than an exception.
Choosing the Right Fix
Adding = null to every property makes the error go away, and that is exactly why it is dangerous. It turns “this value was never set,” which PHP was loudly telling you, into a silent null that fails later with a less helpful error, often a call to a member function on null three files away.
Pick the fix by asking what the property means:
| The property is… | Fix |
|---|---|
| Required for the object to be valid | Assign it in the constructor (promotion is the tidiest way) |
| Optional, with a sensible empty value | Default in the declaration: = [], = '', = 0 |
| Genuinely “may not exist yet” | Nullable with = null, and handle null where it is read |
| Lazily computed | Keep it uninitialized and guard the read |
For the lazy case, isset() and the null coalescing operator both treat an uninitialized property as not set, without throwing:
<?php
class Report
{
private array $rows;
public function rows(): array
{
return $this->rows ??= $this->loadRows();
}
private function loadRows(): array
{
return []; // real query here
}
}
The ??= operator (PHP 7.4 and later) loads the value on first access and reuses it afterwards.
Checking Initialization From the Outside
When you are debugging someone else’s object, for example inside a test, ReflectionProperty::isInitialized() (PHP 7.4 and later, read on 2026-09-25) tells you the state without triggering the error:
<?php
$rp = new ReflectionProperty(Shape::class, 'numberOfSides');
var_dump($rp->isInitialized($shape)); // bool(false) if never assigned
Use it for diagnosis and in tests. In application code, reaching for reflection to check your own object’s state is a sign the class should enforce its own initialization instead.
Catching It, and Why Your catch Block Missed
The thrown object is an Error, not an Exception. A catch (Exception $e) block will not catch it, which surprises people who wrapped the call “just in case.” You would need catch (Error $e) or catch (Throwable $e). In practice, you should not catch this at all. It is a programming mistake, and the right response is to fix the code path, not to hide it at runtime.
AI coding assistants produce this error regularly when they add types to an existing class, because they tend to add ?Type $prop; without = null, assuming the two are equivalent. If a generated diff adds property types, check every one for a default or a constructor assignment before you merge. That is one of the cases where AI-generated code can pass tests and still be wrong: tests that always call the setter first will never hit it.
FAQ
What does “typed property must not be accessed before initialization” mean?
It means code read a property that has a declared type but has never been assigned a value. Typed properties without a default start in an uninitialized state, and reading them throws an Error.
Is an uninitialized typed property the same as null?
No. Only untyped properties behave as null by default. A typed property, even a nullable one like ?string $x;, is uninitialized until assigned. Write ?string $x = null; if you want it to start as null.
Why does the error appear after unserializing or loading from the database?
Serializers and ORMs often create objects without running the constructor. Any property you only assign in the constructor, and that is missing from the stored data, stays uninitialized. Give it a declaration default or initialize it in the relevant hook.
Can I check whether a typed property is initialized?
Yes. isset($obj->prop) returns false for an uninitialized property without throwing, and ReflectionProperty::isInitialized() reports the state directly. The ?? and ??= operators are also safe.
Why did my try/catch not catch it?
Because it throws an Error, not an Exception. Only catch (Error $e) or catch (Throwable $e) catches it. Fixing the missing assignment is almost always better than catching it.

