PHP ValueError
last modified October 22, 2026
In this article, we explore the ValueError class in PHP. A
ValueError is thrown when a function receives an argument of the
correct type but with an invalid value.
Basic definition
ValueError was introduced in PHP 8.0. Before that, most built-in
functions emitted a warning and returned false or
null when given an invalid argument value; since PHP 8.0, many of
them throw a ValueError instead. It complements
TypeError, which is thrown when an argument has the wrong type.
ValueError extends Error, not Exception.
User code may throw it as well, to signal that an argument has an unacceptable
value.
ValueError in built-in functions
The following example triggers a ValueError in several built-in
functions.
<?php
declare(strict_types=1);
$calls = [
'str_repeat' => fn() => str_repeat('ab', -1),
'array_chunk' => fn() => array_chunk([1, 2, 3], 0),
'array_combine' => fn() => array_combine(['a', 'b'], [1]),
'random_int' => fn() => random_int(10, 1),
'explode' => fn() => explode('', 'a,b,c'),
'range' => fn() => range(1, 5, 0),
];
foreach ($calls as $name => $call) {
try {
$call();
} catch (ValueError $e) {
printf("%-14s %s\n", $name, $e->getMessage());
}
}
Each closure calls a function with an argument of the correct type but an invalid value: a negative repeat count, a zero chunk size, arrays of different lengths, a minimum greater than the maximum, an empty separator, and a zero step. The messages name the function and the offending argument.
$ php builtin.php str_repeat str_repeat(): Argument #2 ($times) must be greater than or equal to 0 array_chunk array_chunk(): Argument #2 ($length) must be greater than 0 array_combine array_combine(): Argument #1 ($keys) and argument #2 ($values) must have the same number of elements random_int random_int(): Argument #1 ($min) must be less than or equal to argument #2 ($max) explode explode(): Argument #1 ($separator) must not be empty range range(): Argument #3 ($step) cannot be 0
TypeError versus ValueError
The two errors are easy to tell apart: TypeError concerns the type
of an argument, ValueError its value.
<?php
declare(strict_types=1);
$tests = [
'wrong type' => fn() => str_repeat('x', '3'),
'wrong value' => fn() => str_repeat('x', -3),
];
foreach ($tests as $label => $test) {
try {
$test();
} catch (TypeError | ValueError $e) {
printf("%-12s %-10s %s\n", $label, $e::class, $e->getMessage());
}
}
With strict types enabled, passing the string '3' to
str_repeat is a type error. Passing the integer -3 has
the right type, but the value is out of range. A single catch block
can handle both errors with the | syntax.
$ php type_vs_value.php wrong type TypeError str_repeat(): Argument #2 ($times) must be of type int, string given wrong value ValueError str_repeat(): Argument #2 ($times) must be greater than or equal to 0
The class hierarchy
Because ValueError is an Error, a
catch (Exception $e) block does not catch it.
<?php
declare(strict_types=1);
$e = new ValueError("bad value");
var_dump($e instanceof Error);
var_dump($e instanceof Exception);
var_dump($e instanceof Throwable);
echo implode(' > ', [ValueError::class, ...array_values(class_parents($e))]), "\n";
try {
try {
str_repeat('x', -3);
} catch (Exception $ex) {
echo "never reached\n";
}
} catch (ValueError $ex) {
echo "ValueError is not an Exception: ", $ex->getMessage(), "\n";
}
The inner catch block for Exception is skipped and the
outer block for ValueError handles the error. To catch every
error and exception, use Throwable.
$ php hierarchy.php bool(true) bool(false) bool(true) ValueError > Error ValueError is not an Exception: str_repeat(): Argument #2 ($times) must be greater than or equal to 0
Invalid enum value
The from method of a backed enum throws a ValueError
when no case has the given value.
<?php
declare(strict_types=1);
enum PaymentStatus: string
{
case Pending = 'pending';
case Completed = 'completed';
}
try {
$status = PaymentStatus::from('failed');
} catch (ValueError $e) {
echo "Caught ValueError: ", $e->getMessage(), "\n";
}
$status = PaymentStatus::tryFrom('failed') ?? PaymentStatus::Pending;
echo "Status: ", $status->value, "\n";
The value 'failed' does not match any case of
PaymentStatus. The tryFrom method returns
null instead of throwing, which combines well with the null
coalescing operator to provide a default case.
$ php enum_from.php Caught ValueError: "failed" is not a valid backing value for enum PaymentStatus Status: pending
Invalid JSON depth
Not every problem in a built-in function is reported with a
ValueError. The json_decode function shows the
difference between an invalid argument and invalid data.
<?php
declare(strict_types=1);
try {
json_decode('{"a": 1}', true, 0);
} catch (ValueError $e) {
echo "Caught ValueError: ", $e->getMessage(), "\n";
}
$data = json_decode('{"a": {"b": {"c": 1}}}', true, 2);
var_dump($data);
echo json_last_error_msg(), "\n";
A depth of 0 is an invalid argument, so a ValueError
is thrown. A document that is nested deeper than the allowed depth is a data
problem; json_decode returns null and the error is
available through json_last_error_msg, unless the
JSON_THROW_ON_ERROR flag is used.
$ php json_depth.php Caught ValueError: json_decode(): Argument #3 ($depth) must be greater than 0 NULL Maximum stack depth exceeded
Throwing ValueError in your code
This example validates a discount percentage in an e-commerce system.
<?php
declare(strict_types=1);
function applyDiscount(float $price, float $discount): float
{
if ($discount < 0 || $discount > 100) {
throw new ValueError("Discount must be between 0 and 100");
}
return $price * (1 - $discount / 100);
}
foreach ([20.0, 150.0] as $discount) {
try {
printf("Price: %.2f\n", applyDiscount(50.0, $discount));
} catch (ValueError $e) {
echo "Caught ValueError: ", $e->getMessage(), "\n";
}
}
The applyDiscount function throws a ValueError if the
discount percentage is outside the range of 0 to 100. This ensures that invalid
discounts do not silently produce incorrect prices.
$ php discount.php Price: 40.00 Caught ValueError: Discount must be between 0 and 100
Invalid date
This example validates a booking date in a reservation system.
<?php
declare(strict_types=1);
function bookDate(string $date): DateTimeImmutable
{
$dt = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
if ($dt === false || $dt->format('Y-m-d') !== $date) {
throw new ValueError("Invalid or non-existent date: $date");
}
return $dt;
}
foreach (['2026-04-30', '2026-04-31', '30.04.2026'] as $date) {
try {
$booked = bookDate($date);
echo "Booked: ", $booked->format('l, F j, Y'), "\n";
} catch (ValueError $e) {
echo "Caught ValueError: ", $e->getMessage(), "\n";
}
}
DateTimeImmutable::createFromFormat silently rolls over
non-existent dates, so 2026-04-31 would become May 1. Formatting
the result and comparing it with the input catches such dates as well as input
in a different format. The ! in the format resets the time fields
to zero.
$ php date.php Booked: Thursday, April 30, 2026 Caught ValueError: Invalid or non-existent date: 2026-04-31 Caught ValueError: Invalid or non-existent date: 30.04.2026
Invalid email format
This example validates an email address during user registration.
<?php
declare(strict_types=1);
function registerUser(string $email): void
{
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
throw new ValueError("Invalid email address: $email");
}
echo "User registered with email: $email\n";
}
foreach (['jane@example.com', 'user@.com'] as $email) {
try {
registerUser($email);
} catch (ValueError $e) {
echo "Caught ValueError: ", $e->getMessage(), "\n";
}
}
The filter_var function with FILTER_VALIDATE_EMAIL
returns false for an invalid address, and the function turns this
into a ValueError.
$ php email.php User registered with email: jane@example.com Caught ValueError: Invalid email address: user@.com
Invalid currency code
This example validates a currency code against a list of supported codes.
<?php
declare(strict_types=1);
function setCurrency(string $code): string
{
$validCurrencies = ['USD', 'EUR', 'GBP'];
if (!in_array($code, $validCurrencies, true)) {
throw new ValueError(sprintf("Invalid currency code '%s', expected one of: %s",
$code, implode(', ', $validCurrencies)));
}
return $code;
}
foreach (['EUR', 'JPY'] as $code) {
try {
$currency = setCurrency($code);
echo "Currency set to: $currency\n";
} catch (ValueError $e) {
echo "Caught ValueError: ", $e->getMessage(), "\n";
}
}
The in_array check is called with true as the third
argument, which enables strict comparison. The error message lists the
accepted values, which helps the caller fix the input.
$ php currency.php Currency set to: EUR Caught ValueError: Invalid currency code 'JPY', expected one of: USD, EUR, GBP
ValueError or InvalidArgumentException
Many libraries signal invalid arguments with the
InvalidArgumentException class from the SPL.
<?php
declare(strict_types=1);
function setStockQuantity(int $quantity): int
{
if ($quantity < 0 || $quantity > 1000) {
throw new InvalidArgumentException("Stock quantity must be between 0 and 1000, got $quantity");
}
return $quantity;
}
try {
setStockQuantity(-5);
} catch (Exception $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
InvalidArgumentException is an Exception, so code
that catches Exception handles it. Use ValueError
when you want to mirror the behaviour of built-in functions, and
InvalidArgumentException (or a custom exception) for
recoverable validation errors in application code. Whichever you choose, be
consistent across a project.
$ php invalid_argument.php InvalidArgumentException: Stock quantity must be between 0 and 1000, got -5
Source
PHP ValueError - Documentation
In this article, we have shown when PHP throws ValueError, how it
differs from TypeError and Exception, and how to use
it for value checks in our own code.
Author
List all PHP tutorials.