The Secret Life of “Magic Null” in PHP
PHP 8 has started a tightening in native functions arguments: every version brings stricter checks on calls to common functions. Gone are the days of passing null into a function expecting an integer and hoping for the best. Today, usort($arr, null) or array_walk($arr, null) will instantly crash your app with a beautiful, unapologetic TypeError. strict_types for the win.
The core team has been systematically purging null from native function signatures. And honestly, thank goodness. It makes code predictable, consistent and just plain nicer.
And, as per PHP traditions, deep within the C-level bowels of the engine, a small village of irreducible resists again and agin (mandatory * pun). They aren’t just using null as a lazy default fallback. They use what I call Magic Null a deliberate sentinel value that configures what a function does, in a way that’s meaningfully different from passing zero, an empty string, or any other empty-ish value.
These are rare, slightly quirky, but incredibly useful once you know they exist. Let’s look at the holdouts.
1. array_map(null, ...), The Unnamed Zipper
Normally, array_map takes a callback and apply it to one or more arrays. But if you pass null as the callback, PHP abandons the mapping concept entirely and turns into a zipping function. Nothing about compression, mind you, but rather an array building feature. It takes the first element of every array, puts them in a new array, then the second element, and so on.
<?php $keys = ['id', 'name']; $values = [1, 'Alice']; $zipped = array_map(null, $keys, $values); // Result: [['id', 1], ['name', 'Alice']] ?>
This is the native way to combine arrays in PHP without writing a loop. It is the opposite of array_column.
<?php $source = [['id', 1], ['name', 'Alice']]; $keys = array_column($source, 0); // ['id', 'name'] $values = array_column($source, 1); // [1, 'Alice'] $zipped = array_map(null, $keys, $values); // Result: [['id', 1], ['name', 'Alice']] === $source ?>
All this work on arrays, but consider them for list rather than hash or maps.
2. array_filter($arr, null): The Empty Filter
You’ve probably used array_filter with a closure a thousand times to remove empty values. But you can skip the closure entirely. Passing null as the second argument triggers a specific code path that automatically filters out all falsy values (false, 0, '', null, []), using empty().
<?php $data = ['apple', '', 'banana', null, 0, 'cherry']; $filtered = array_filter($data, null); // Result: ['apple', 'banana', 'cherry'] ?>
It’s a tiny shortcut, but it reads beautifully: Filter this array, using nothing.
3. array_column($arr, null, $key): The Instant Hashmap
array_column is famous for extracting a single column from a 2D array into a flat list: we have seen this above. But if you pass null as the column argument, it doesn’t extract anything. Instead, it returns the entire 2D array, and re-indexes it by the column you specified in the third argument.
<?php
$users = [
['id' =>101, 'name' =>'Alice'],
['id' =>102, 'name' =>'Bob'],
];
$lookup = array_column($users, null, 'id');
// Result: [101 =>['id' =>101, 'name' =>'Alice'], ...]
?>
This is the absolute fastest way to turn a sequential database result set into an O(1) lookup dictionary.
4. array_slice($arr, $offset, null): The Unlimited Slice
array_slice‘s third argument, $length, looks like an ordinary number: pass 3 and you get 3 elements, pass 0 and, logically enough, you get nothing back.
null breaks that pattern on purpose. It doesn’t mean zero elements: it means no limit, take everything to the end of the array. It’s the one value in that parameter that isn’t a length at all; it’s an instruction to ignore the length concept entirely. The confusing part is that most often, null is 0.
<?php $a = [1, 2, 3, 4, 5]; array_slice($a, 1, null); // [2, 3, 4, 5] <- everything to the end array_slice($a, 1, 0); // [] <- an empty slice ?>
The gotcha this saves you from: reaching for 0 when you mean no limit and silently getting an empty array back instead. mb_substr() and array_splice() follow the exact same convention for their length parameters.
5. error_reporting(null): The Silent Peek
A handful of PHP’s configuration-style functions overload a single parameter to do double duty as both a getter and a setter. error_reporting() is the clearest example: pass an actual error-reporting level and it changes the setting; pass null and it just tells you the current setting, untouched.
The important part is that null here is not just the default: it behaves completely differently from the other falsy value you’d reach for, 0, which is a real, valid error-reporting level that turns off all error reporting.
<?php error_reporting(E_ALL); error_reporting(null); // 30719 — reads the current level, changes nothing error_reporting(0); // 30719 — but returns the *old* level, and actually sets it to 0! ?>
Call this twice in a row and you get very different systems: one still reporting every error, one reporting none. Confusing error_reporting(null) with error_reporting(0) is an easy, very real bug.
The same “pass null to peek, pass a value to poke” convention shows up across a whole family of PHP functions: ignore_user_abort(null), mb_internal_encoding(null), mb_regex_encoding(null), libxml_use_internal_errors(null), and the session configuration functions like session_cache_limiter(null), session_save_path(null), and session_name(null). None of them require an argument to work as a getter, but all of them accept an explicit null as a deliberate, documented way to say just checking.
6. set_error_handler(null): The Reset Call
You’d think passing null to an error handler setter would either turn error handling off, or undo your last change and restore whatever handler was active before. It does neither. The manual is explicit: passing null “resets the handler to its default state”: meaning PHP’s own built-in error handler, not the handler you had active a moment ago. It boils down to the fact that there must be an error handler at all time. The king is dead, long life the king!
<?php
set_error_handler('handlerA');
set_error_handler('handlerB');
set_error_handler(null); // Neither handlerA nor handlerB run now—PHP's native
// error output takes over.
restore_error_handler(); // THIS is what brings handlerB back.
restore_error_handler(); // And this brings handlerA back.
?>
There genuinely is an internal handler stack, and set_error_handler() does push the previous handler onto it every time you call it, including when you pass null: so the instinct that something stack-like is happening isn’t wrong. But null itself is a reset, not an undo. The undo button is restore_error_handler(). Mixing the two up is a documented gotcha: the manual’s own user notes warn that repeatedly calling set_error_handler(null) instead of restore_error_handler() silently leaks entries onto that stack. The same behavior applies to set_exception_handler(null).
An RFC for the Future: usort($array, null)
Put the five survivors above side by side and a real, load-bearing pattern falls out: PHP already treats null as a legitimate way to say skip the custom behavior, use the obvious built-in default instead as with array_map, array_filter, array_column, array_slice, or don't change anything, just tell me the current state as in error_reporting and its family, or reset to a known baseline as in set_error_handler. This isn’t a handful of accidents: it’s a convention from the ages before.
usort(), uasort(), and uksort() are the odd ones out. Their callback parameter is a plain, non-nullable callable, so usort($arr, null), the exact call this article opened by warning you about, throws a TypeError on every currently supported version of PHP:
<?php usort($nums, null); // TypeError: usort(): Argument #2 ($callback) must be a valid callback, no array or string given ?>
But what should no custom comparator mean for a sort function? PHP already has an answer, sitting right next to it in the standard library: plain sort(), which orders elements using PHP’s built-in comparison rules, effectively <=>, the famous spaceship operator. There’s no ambiguity to design around: the default already exists and is one function call away.
The RFC pitch: make callback nullable on usort()/uasort()/uksort(), and let null fall through to the same comparator sort()/asort()/ksort() already use internally.
<?php $nums = [3, 1, 2]; // Today, this is what "no custom comparator" looks like in practice usort($nums, fn($a, $b) =>$a <=>$b); // Proposed: same result, consistent with array_map(null, ...) and // array_filter($arr, null), and no TypeError to work around usort($nums, null); ?>
It doesn’t need to do anything a developer can’t already do in one line: array_filter($arr, null) doesn’t do anything you couldn’t write yourself with array_filter($arr, fn($v) =>(bool) $v) either. The value is in turning a common idiom into the documented default, and in making null a legitimate, intentional argument to usort() instead of the trap this article’s own opening paragraph warns you it currently is.

