PHP 8.4 TypeError and ArgumentCountError Playbook: What Breaks and How to Fix It
PHP 8.4 introduces stricter type and error handling, converting many E_WARNING messages into hard-throwing TypeError and ArgumentCountError exceptions. I built this playbook for identifying potential issues and mitigating them before upgrading.
The problem: more exceptions, less warning
Historically, PHP has been lenient with type mismatches and incorrect argument counts for internal functions, often resulting in warnings, notices, or silent buggy behavior. With PHP 8.4, the engine throws hard exceptions instead.
Code that "works with warnings" in PHP 8.3 can fatally crash in PHP 8.4. A count(null) that returned 0 with a warning now throws a TypeError.
The error handling shift
The solution: a proactive mitigation strategy
Phase 1: static analysis
# For PHPStan
vendor/bin/phpstan analyse src/ --level=max
# For Psalm
vendor/bin/psalm --level=1
Pay close attention to errors related to:
- Invalid types passed to core functions
- Incorrect argument counts
- Usage of arithmetic operators on non-numeric types
Phase 2: targeted code review
- TypeError Changes
- ArgumentCountError Changes
| Change Description | Before (PHP < 8.4) | After (PHP 8.4) | Mitigation |
|---|---|---|---|
count() on invalid types | E_WARNING | TypeError | Use is_countable() before calling count() |
| Arithmetic/Bitwise on arrays/objects | E_WARNING | TypeError | Ensure operands are numeric or cast explicitly |
| Illegal string offset | E_WARNING | TypeError | Validate array keys before access; use array_key_exists() |
exit()/die() with invalid type | Inconsistent | TypeError | Only pass string or int arguments |
| Magic method type checks | No strict check | TypeError if declared types mismatch | Add or correct type hints for magic methods |
| Change Description | Before (PHP < 8.4) | After (PHP 8.4) | Mitigation |
|---|---|---|---|
| Wrong argument count for built-in functions | E_WARNING (since 7.1) / ArgumentCountError (since 8.0) | ArgumentCountError | Review all calls to built-in PHP functions and ensure argument count is correct |
Phase 3: fix the code
count() on a non-countable variable:
- $value = null;
- $count = count($value); // Returns 0 with warning
+ $value = null;
+ $count = is_countable($value) ? count($value) : 0;
Arithmetic on an array:
- $items = [1, 2];
- $new_items = $items + 1; // null with warning
+ $items = [1, 2];
+ $items[] = 1; // Append element instead
Full code examples
// count() on null -- returns 0 with E_WARNING
$value = null;
$count = count($value);
$value = null;
if (is_countable($value)) {
$count = count($value);
} else {
$count = 0;
}
// Array + int -- E_WARNING, result is null
$items = [1, 2];
$new_items = $items + 1;
// If you meant to add an element, use array_push or []
$items = [1, 2];
$items[] = 1;
PHP version compatibility matrix
| Behavior | PHP 8.1 | PHP 8.2 | PHP 8.3 | PHP 8.4 |
|---|---|---|---|---|
count(null) | Warning + 0 | Warning + 0 | Warning + 0 | TypeError |
| Array arithmetic | Warning | Warning | Warning | TypeError |
| Illegal string offset | Warning | Warning | Warning | TypeError |
| Wrong arg count (built-in) | ArgumentCountError | ArgumentCountError | ArgumentCountError | ArgumentCountError |
| Implicit nullable params | Works | Works | Works | Deprecated |
exit() with invalid type | Inconsistent | Inconsistent | Inconsistent | TypeError |
A gradual upgrade through minor versions (8.1, 8.2, 8.3) first can make the final jump to 8.4 much smoother by addressing deprecations and changes incrementally.
Migration checklist
- Run PHPStan at
--level=maxon all custom code - Run Psalm at
--level=1on all custom code - Search for
count()calls on potentially null/non-countable variables - Audit arithmetic operations for non-numeric operands
- Review magic method type declarations
- Check all
exit()/die()calls for valid argument types - Run full test suite on PHP 8.4 in CI
- Fix all TypeError and ArgumentCountError findings before production deploy
Quick grep patterns to find risky code
# Find count() on variables that might be null
grep -rn 'count(\$' src/ | head -20
# Find arithmetic on array variables
grep -rn '\$.*\[\].*[+\-\*\/]' src/ | head -20
# Find exit/die with variables
grep -rn 'exit(\$\|die(\$' src/ | head -20
Why this matters for Drupal and WordPress
Drupal 12 will require PHP 8.4 as its minimum, and WordPress is actively testing PHP 8.4 compatibility. Both ecosystems have large codebases of contrib modules and plugins that use count() on potentially null values, arithmetic on mixed types, and loose argument counts to internal functions. Drupal sites should run PHPStan against custom modules and contributed code before upgrading. WordPress plugin developers should add PHP 8.4 to their CI test matrix now -- the count(null) TypeError alone will crash plugins that pass unvalidated query results to count functions.
What I learned
- Proactive static analysis is non-negotiable. Running tools like PHPStan and Psalm at their strictest levels is the most effective first line of defense.
- The
is_countable()function is your new best friend. Its usage should become standard practice before callingcount()on any variable whose type is not guaranteed. - PHP is continuing its journey toward stricter typing. These changes are not isolated; they are part of a larger trend. Embracing explicit typing now will save headaches later.
- Do not just suppress errors, understand the intent. When you find code that triggers these new exceptions, the goal is to understand the original developer's intent and fix the underlying logical flaw.
References
- PHP 8.4: Tighter Type and Error Handling
- PHP RFC: Stricter implicit boolean coercions
- Drupal 12 Readiness Dashboard
Looking for an Architect who doesn't just write code, but builds the AI systems that multiply your team's output? View my enterprise CMS case studies at victorjimenezdev.github.io or connect with me on LinkedIn.
