PHP Arrow Functions (fn)
An arrow function, written with the fn keyword, is a compact syntax for creating a closure from a single expression. Instead of writing function ($x) use ($y) { return $x + $y; }, you can write fn($x) => $x + $y and PHP automatically makes $y available inside the function. Arrow functions matter because so much everyday PHP code passes tiny one-line callbacks to functions like array_map, array_filter, and usort — arrow functions make those callbacks shorter, more readable, and free of manual variable importing.
Overview: How Arrow Functions Work
Arrow functions were added in PHP 7.4 as a shorthand for anonymous functions (closures) whose body is a single expression. A closure created with the function keyword only sees variables you explicitly import with a use clause; everything else is invisible to it, which is why you constantly see code like function ($x) use ($multiplier) { return $x * $multiplier; }. An arrow function skips that ceremony: PHP’s compiler scans the expression body, finds every variable from the enclosing scope that the expression references, and automatically captures each one by value, exactly as if you had written use ($multiplier) yourself.
Internally, an arrow function is still compiled into an instance of the built-in Closure class — there is no separate “arrow function” object type at runtime. When the Zend engine parses fn(...) => expr, it treats expr as an implicit return expr; statement, wraps it in an anonymous function body, and performs a static-analysis pass over the expression to discover which outer variables it touches. Those variables become hidden, automatically generated use bindings on the resulting closure object. Because the values are captured at the moment the fn expression is evaluated (i.e. when that line of code actually runs), reassigning the outer variable afterward has no effect on a closure that has already been created — a detail that trips up many newcomers (see Common Mistakes below).
Arrow functions also automatically bind $this when defined inside a class method, just like regular closures do by default, and they support everything a normal function signature supports: type hints, default values, variadics, by-reference parameters, and return type declarations. What they do not support is a multi-statement body — the part after => must be exactly one expression.
Syntax
The general form of an arrow function is:
fn(parameter_list): return_type => expression
| Part | Meaning |
|---|---|
fn |
The keyword that starts an arrow function expression (always written lowercase by convention). |
(parameter_list) |
A normal parameter list — type hints, default values, variadics (...$rest), and by-reference (&$x) parameters are all allowed. |
: return_type |
An optional return type declaration, written after the parameter list, just like on a normal function. |
=> |
Separates the signature from the body. |
expression |
A single expression. Its value is implicitly returned — you never write return or wrap it in braces. |
Here is a complete example that uses a type-hinted parameter, a default value, and a return type:
<?php
$sum = fn(int $a, int $b = 10): int => $a + $b;
echo $sum(5);
echo PHP_EOL;
echo $sum(5, 20);
Output:
15
25
The first call uses the default value of $b (10), giving 5 + 10 = 15. The second call overrides it with 20, giving 5 + 20 = 25.
Examples
Example 1: A Simple Transformation
<?php
$double = fn($x) => $x * 2;
echo $double(5);
Output:
10
This is the simplest possible arrow function: it takes one parameter and returns twice its value. There is no return keyword and no curly braces — the expression to the right of => is automatically returned.
Example 2: Automatic Capture of an Outer Variable
<?php
$factor = 3;
$multiply = fn($x) => $x * $factor;
echo $multiply(4);
Output:
12
Notice that $multiply never declares use ($factor). Because arrow functions automatically import any outer variable referenced in the expression, $factor is available inside without any extra syntax. With a regular closure you would have had to write function ($x) use ($factor) { return $x * $factor; } to get the same result.
Example 3: A Realistic Use with array_map
<?php
$prices = [10, 25, 40, 5];
$tax = 0.2;
$withTax = array_map(fn($price) => $price * (1 + $tax), $prices);
echo implode(', ', $withTax);
Output:
12, 30, 48, 6
This is the situation arrow functions were designed for: a short, throwaway callback passed straight into a built-in array function. Each price is multiplied by 1 + $tax (1.2), and $tax is captured automatically from the surrounding scope without an explicit use clause.
Example 4: Nested Arrow Functions (Currying)
<?php
$makeAdder = fn($x) => fn($y) => $x + $y;
$add5 = $makeAdder(5);
echo $add5(10);
Output:
15
Arrow functions can return other arrow functions. Here, calling $makeAdder(5) produces a new closure that has captured $x = 5; calling that closure with 10 adds the two together. This pattern, called currying, turns a two-argument function into a chain of one-argument functions, and it works because each nested fn can see the variables captured by the fn that encloses it.
How It Works Step by Step (Under the Hood)
When the engine encounters an arrow function expression, it performs roughly these steps:
- The parser recognizes the
fn(...) => exprpattern and treatsexpras an implicitreturn expr;inside a new anonymous function body. - Before compiling that body, PHP statically walks the expression looking for any variable that is not a parameter — for example
$factorin Example 2. - Every such variable is added to the generated closure as an automatic, by-value capture, exactly as if you had typed it in a
use (...)list on a normal closure. - The engine evaluates those captures immediately, at the point where the
fnexpression itself is executed — not later, when the closure is eventually called. This is why reassigning the outer variable afterward doesn’t change what an already-created arrow function will use. - The result is an ordinary
Closureobject. Calling it later runs the compiled expression using the parameter values passed in plus the values captured earlier, and returns the expression’s result.
Because the runtime representation is a normal Closure, arrow functions are fully compatible with anything that accepts a callable: array_map, usort, array_filter, Closure::fromCallable, and type hints of callable or Closure all work exactly as they would with a function created via function (...) {...}.
Common Mistakes
Mistake 1: Expecting a Live (By-Reference) Capture
<?php
$x = 10;
$fn = fn() => $x;
$x = 20;
echo $fn();
Output:
10
Many developers expect this to print 20, assuming the arrow function always reads the “current” value of $x. It doesn’t. Capturing happens by value at the moment the arrow function is created — before $x is reassigned to 20 — so the closure permanently remembers 10. If you genuinely need a live reference, you must fall back to a normal closure with an explicit by-reference use (&$x).
Mistake 2: Trying to Use a Multi-Statement Body
$double = fn($x) => {
$y = $x * 2;
return $y;
};
This is a syntax error. The part after => must be a single expression — a block wrapped in braces is not a valid expression, so PHP refuses to parse this at all. The fix is to either keep it to one expression or switch to a regular anonymous function:
<?php
$double = function ($x) {
$y = $x * 2;
return $y;
};
echo $double(21);
Output:
42
If the logic can be reduced to one expression, an arrow function still works fine here — for example fn($x) => $x * 2 — but as soon as you need an intermediate variable, a conditional with multiple branches of logic, or more than one statement, reach for function instead.
Best Practices
- Use arrow functions for short, throwaway callbacks passed to functions like
array_map,array_filter, andusort— that’s exactly the case they were designed to shorten. - Remember that captured variables are snapshotted by value when the
fnexpression runs, not when it is later called — don’t rely on seeing later changes to an outer variable. - Switch to a full
function (...) { ... }closure as soon as the logic needs more than one statement, a loop, or multiplereturnpaths — don’t force complex logic into a single expression just to keep it “short”. - Add type hints and a return type to arrow functions used in public APIs or reusable helpers; they read exactly like a normal function signature and cost nothing extra.
- Prefer
static fn(...) => ...when the arrow function doesn’t need$this, to avoid an unnecessary object binding and make the intent explicit. - Avoid deeply nesting arrow functions (
fn() => fn() => fn() => ...) purely for style — a couple of levels for currying is fine, but beyond that it becomes hard to read.
Practice Exercises
- Write an arrow function named
$squarethat returns the square of a number, then use it witharray_mapon the array[1, 2, 3, 4]. Expected output after joining withimplode:1, 4, 9, 16. - Given an array of associative arrays representing people with
nameandagekeys, useusortwith an arrow function to sort the array byagein ascending order. - Write a curried arrow function
$multiplyBysuch that$multiplyBy(3)(4)returns12, following the pattern shown in Example 4.
Summary
fn(params) => exprcreates a closure whose body is exactly one expression, implicitly returned.- Outer variables referenced in the expression are captured automatically, by value — no
useclause needed. - Capturing happens when the
fnexpression is evaluated, not when the resulting closure is later called — reassigning the outer variable afterward doesn’t change it. - Arrow functions compile down to ordinary
Closureobjects and work anywhere a callable is expected. - They cannot contain multiple statements or braces — use a regular
functionclosure once logic grows beyond one expression. - Arrow functions automatically bind
$thisinside methods; usestatic fnwhen that binding isn’t needed.
