PHP Return Types
A return type declaration tells PHP, and every developer who reads your code, exactly what kind of value a function or method will hand back when it finishes. Instead of guessing from a comment or reading through the whole function body, callers can trust the signature itself. Return types make bugs surface early: PHP throws a clear TypeError the moment a function tries to return something that does not match, instead of letting a wrong value silently drift through your program. PHP 8.3’s type system is rich enough to express almost any return shape you need, from simple scalars to unions, nullable types, and special types like void and never.
Overview: How Return Types Work
Before PHP 7, a function’s return value had no declared type at all – you could return an integer from one branch and an array from another, and PHP would happily let both through. Starting with PHP 7.0, you can add a : Type suffix after a function’s parameter list to declare exactly what the function promises to return. PHP 7.1 added nullable types (?Type) and the void type. PHP 8.0 introduced union types such as int|string, and PHP 8.1 added the never type. PHP 8.2 rounded things out with stand-alone true, false, and null types.
Internally, when the Zend Engine compiles a function, it attaches the return type information to the function’s op array as part of its signature. Every time the function executes a return statement, the engine emits a ZEND_VERIFY_RETURN_TYPE check before control passes back to the caller. This check compares the actual value being returned against the declared type. If they match, execution continues normally. If they do not match, PHP tries to coerce the value, but only when the file is running in weak typing mode (the default, without declare(strict_types=1)) and only for scalar types (int, float, string, bool). A numeric string like \"8\" can be coerced into an int, and an int can always widen to a float even in strict mode, but an array can never be coerced into a string. If coercion is not possible, PHP throws a TypeError immediately, not a warning and not a silent failure, but a hard, catchable exception.
Return types are part of a function’s contract. They let editors and static analyzers understand your code, and they let you catch a whole category of bugs, \”this function returned the wrong thing\”, at the exact line where it happens instead of somewhere deep in the call stack.
Syntax
function functionName(paramType $param): ReturnType {
// ...
return $value;
}
ReturnTypeis written after a colon, following the closing parenthesis of the parameter list.- Can be a scalar type:
int,float,string,bool. - Can be a compound type:
array,iterable,object,callable. - Can reference a class, interface, or enum name directly.
- Can be prefixed with
?to allownullin addition to the given type, for example?string. - Can be a union of multiple types joined with
|, for exampleint|string. selfrefers to the class the method is defined in;staticrefers to the class actually instantiated, which matters for inheritance.voidmeans the function returns no meaningful value (an emptyreturn;or no return at all is allowed).nevermeans the function never returns control to the caller at all – it always throws or terminates the script.mixedaccepts any type, the type-system equivalent of no type checking.
| Return Type | Meaning | Example |
|---|---|---|
int |
A single integer value | function age(): int |
?string |
A string, or null |
function name(): ?string |
int|float |
Either an int or a float | function total(): int|float |
void |
No return value | function log(string $m): void |
never |
Never returns (throws or exits) | function abort(): never |
static |
The called class, not the defining class | function make(): static |
Examples
Example 1: A Simple Scalar Return Type
<?php
function add(int $a, int $b): int {
return $a + $b;
}
echo add(4, 6);
echo PHP_EOL;
var_dump(add(2, 3));
Output:
10
int(5)
The : int after the parameter list guarantees that add() always hands back an integer. Because 4 + 6 and 2 + 3 are already integers, no coercion is needed here, but if you tried to return \"not a number\"; instead, PHP would throw a TypeError the moment that line executed.
Example 2: Nullable and Union Return Types
<?php
declare(strict_types=1);
function findUser(int $id): array|null {
$users = [1 => ['name' => 'Ana'], 2 => ['name' => 'Bo']];
return $users[$id] ?? null;
}
$user = findUser(1);
var_dump($user);
$missing = findUser(99);
var_dump($missing);
Output:
array(1) {
[\"name\"]=>
string(3) \"Ana\"
}
NULL
Here the return type is array|null. PHP also lets you write this as the shorthand ?array, but a union like array|null reads the same way and is required once you have more than two possible types. With declare(strict_types=1) active, no coercion happens at all: whatever the function returns must already be exactly one of the declared types.
Example 3: void, never, self, and static
<?php
class Counter {
private int $count = 0;
public function increment(): void {
$this->count++;
}
public function getCount(): int {
return $this->count;
}
public static function create(): self {
return new self();
}
}
function fail(string $message): never {
throw new RuntimeException($message);
}
$counter = Counter::create();
$counter->increment();
$counter->increment();
echo $counter->getCount();
echo PHP_EOL;
try {
fail('Something went wrong');
} catch (RuntimeException $e) {
echo $e->getMessage();
}
Output:
2
Something went wrong
increment() is declared void because it mutates state and returns nothing useful – PHP refuses to let you write return $this->count; inside it. create() returns self, meaning an instance of the class this method is defined in. The free function fail() is declared never: it always throws, so it never actually reaches a return statement, and PHP would flag it as an error if it ever tried to return normally.
Under the Hood: What PHP Actually Does
PHP does not fully verify return types while parsing – that happens at compile time only for obvious syntax errors. The real type check happens at runtime, step by step, every single time the function is called:
- The function body executes normally until it hits a
returnstatement, or falls off the end, which behaves likereturn null;. - The engine evaluates the expression after
returnto produce a value. - The
ZEND_VERIFY_RETURN_TYPEopcode compares that value’s actual type against the declared return type. - If they already match exactly, the value is handed back to the caller with no extra work.
- If they do not match and the file is in weak typing mode, PHP attempts a scalar coercion, for example converting the string
\"8\"to the integer8. - If coercion is impossible, or the file has
declare(strict_types=1), PHP throws aTypeErrorthat propagates up exactly like any other exception – you can catch it with a try/catch block.
<?php
function version(): int {
return \"8\";
}
var_dump(version());
Output:
int(8)
This is why declare(strict_types=1) is so widely recommended: it removes an entire class of \”it worked, but only because PHP quietly converted my data\” bugs, forcing you to convert values explicitly and intentionally instead.
Common Mistakes
Mistake 1: Not returning a value on every code path
If a function declares a non-nullable return type, every possible path through the function must return a matching value, including the \”nothing else matched\” path.
function getStatus(int $code): string {
if ($code === 200) {
return 'OK';
}
// no return here for any other $code value -
// PHP throws: 'must be of type string, none returned'
}
The fix is to make sure every branch, including the default case, returns something of the declared type. A match expression is a clean way to guarantee that:
<?php
function getStatus(int $code): string {
return match (true) {
$code === 200 => 'OK',
$code === 404 => 'Not Found',
default => 'Unknown',
};
}
echo getStatus(404);
Output:
Not Found
Mistake 2: Forgetting to mark a type nullable
A very common bug is declaring a return type like string for a function that can legitimately return null, for example a \”find\” function that may not find anything.
function findName(int $id): string {
$names = [1 => 'Ana'];
return $names[$id] ?? null;
}
Calling this function with an id that is not in the array throws a TypeError, because null does not satisfy string. The declared type must honestly reflect every value the function can produce, so add a leading ?:
<?php
function findName(int $id): ?string {
$names = [1 => 'Ana'];
return $names[$id] ?? null;
}
var_dump(findName(1));
var_dump(findName(5));
Output:
string(3) \"Ana\"
NULL
Best Practices
- Declare a return type on every function and method you write, especially public API functions other developers will call.
- Use
declare(strict_types=1)at the top of your files so coercion bugs turn into loud, catchableTypeErrors instead of silent conversions. - Prefer the shorthand
?Typeover a two-member union likeType|null; they behave identically, but?Typereads faster. - Reserve
voidfor functions whose entire purpose is a side effect, such as logging, mutating state, or printing; if a caller might ever want the result, return something. - Use
neverto document functions that always throw or terminate, such as guard clauses or fatal error handlers; it also lets static analyzers flag unreachable code after the call. - Use
staticinstead ofselffor factory methods on classes that might be extended, so subclasses get back an instance of themselves rather than the parent. - Avoid
mixedunless a function genuinely has to handle arbitrary types; it disables the safety net return types are meant to provide. - Keep return types as narrow and specific as possible; a wide union such as
int|string|array|nullis often a sign the function is doing too much.
Practice Exercises
- Write a function
square(int $n): intthat returns the square of its argument, then call it with5and print the result. - Write a function
divide(float $a, float $b): ?floatthat returnsnullif$bis0, and the division result otherwise. Test it with both a valid division and a division by zero. - Write a function
requireEnv(string $name): neverthat always throws aRuntimeExceptionstating the environment variable is missing. Then write a second function that calls it only when a given array key is absent, and returns the value when it is present.
Summary
- A return type is declared with a colon after a function’s parameter list, for example
function name(): int. - PHP checks the declared type every time a
returnstatement executes, using theZEND_VERIFY_RETURN_TYPEcheck. - In weak typing mode PHP tries to coerce scalar mismatches; with
declare(strict_types=1)it does not, and throws aTypeErrorinstead. - Use
?Typeor a union likeType|nullfor functions that can return nothing meaningful,voidfor functions with no return value at all, andneverfor functions that always throw or exit. selfreturns an instance of the defining class;staticreturns an instance of whatever class was actually called, which matters in inheritance.- Every code path in a function must satisfy the declared return type, or PHP throws a
TypeErrorat runtime.
