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;
}
  • ReturnType is 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 allow null in addition to the given type, for example ?string.
  • Can be a union of multiple types joined with |, for example int|string.
  • self refers to the class the method is defined in; static refers to the class actually instantiated, which matters for inheritance.
  • void means the function returns no meaningful value (an empty return; or no return at all is allowed).
  • never means the function never returns control to the caller at all – it always throws or terminates the script.
  • mixed accepts 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 return statement, or falls off the end, which behaves like return null;.
  • The engine evaluates the expression after return to produce a value.
  • The ZEND_VERIFY_RETURN_TYPE opcode 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 integer 8.
  • If coercion is impossible, or the file has declare(strict_types=1), PHP throws a TypeError that 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, catchable TypeErrors instead of silent conversions.
  • Prefer the shorthand ?Type over a two-member union like Type|null; they behave identically, but ?Type reads faster.
  • Reserve void for 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 never to 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 static instead of self for factory methods on classes that might be extended, so subclasses get back an instance of themselves rather than the parent.
  • Avoid mixed unless 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|null is often a sign the function is doing too much.

Practice Exercises

  1. Write a function square(int $n): int that returns the square of its argument, then call it with 5 and print the result.
  2. Write a function divide(float $a, float $b): ?float that returns null if $b is 0, and the division result otherwise. Test it with both a valid division and a division by zero.
  3. Write a function requireEnv(string $name): never that always throws a RuntimeException stating 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 return statement executes, using the ZEND_VERIFY_RETURN_TYPE check.
  • In weak typing mode PHP tries to coerce scalar mismatches; with declare(strict_types=1) it does not, and throws a TypeError instead.
  • Use ?Type or a union like Type|null for functions that can return nothing meaningful, void for functions with no return value at all, and never for functions that always throw or exit.
  • self returns an instance of the defining class; static returns 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 TypeError at runtime.