Step 1 of 7 · Reading · ~6 min
Learn
Classes, Inheritance, Errors
Errors and Exceptions
Something goes wrong: a file is missing, a number is not a number, a divisor is zero. PHP's answer is to throw — abandon the current path and hand a value describing the failure to whoever is prepared to deal with it. This lesson covers throwing, catching, and the two things beginners get wrong most often: which block runs when, and which type to catch.
Defining your own exception types, and the hierarchy they slot into, is the next lesson.
Throwing exceptions
<?php
function parseAge(string $s): int {
if ($s === '') {
throw new InvalidArgumentException('empty input');
}
if (!is_numeric($s)) {
throw new InvalidArgumentException("not a number: $s");
}
$n = (int) $s;
if ($n < 0) {
throw new RangeException("negative age: $n");
}
return $n;
}
throw new SomeException("message") abandons the function on the spot and unwinds outward until something catches it. The ones you will reach for:
| Class | Use it for |
|---|---|
Exception | the generic base for your own error types |
LogicException | a bug in the calling code |
InvalidArgumentException | an argument that should never have been passed |
RangeException, DomainException | a value outside what the function accepts |
RuntimeException | a failure only detectable while running |
Two more are raised by the engine rather than by you:
TypeError— PHP 7.0+, thrown when an argument or a return value does not match a declared type. It works on this grader.ValueError— PHP 8.0+, thrown when an internal function is handed the right type but an impossible value. It does not exist on this 7.4 grader:class_exists('ValueError')answersfalsehere, so never write acatchfor it in these exercises.
try / catch / finally
<?php
foreach (['37', 'kitten', '-4'] as $input) {
try {
$age = parseAge($input);
echo "got $age\n";
} catch (InvalidArgumentException $e) {
echo "bad input: ", $e->getMessage(), "\n";
} catch (RangeException $e) {
echo "out of range: ", $e->getMessage(), "\n";
} finally {
echo " (checked)\n";
}
}
//> got 37
//> (checked)
//> bad input: not a number: kitten
//> (checked)
//> out of range: negative age: -4
//> (checked)
catchbranches are tried most specific first.InvalidArgumentExceptionextendsLogicException, so acatch (LogicException $e)placed above would quietly swallow both.finallyruns on every path — clean return, caught exception, even areturnfrom inside thetry.$e->getMessage()gives back the text you passed to the constructor.
One trap worth meeting now. echo prints its arguments one at a time, so a throw partway along leaves the earlier part already written to standard output:
<?php
try {
echo "got ", parseAge('kitten'), "\n";
} catch (InvalidArgumentException $e) {
echo "bad input: ", $e->getMessage(), "\n";
}
//> got bad input: not a number: kitten
Compute first, print second, and a byte-exact grader never sees a half-written line.
Multi-catch: one handler, several types
<?php
function classify(int $n): string {
if ($n < 0) {
throw new RangeException("negative");
}
if ($n > 150) {
throw new DomainException("implausible");
}
return "ok";
}
foreach ([42, -1, 900] as $n) {
try {
echo classify($n), "\n";
} catch (RangeException | DomainException $e) {
echo "rejected: ", $e->getMessage(), "\n";
}
}
//> ok
//> rejected: negative
//> rejected: implausible
The pipe form arrived in PHP 7.1, so it runs perfectly well on this course's PHP 7.4 grader. Use it when several exception types deserve identical handling, and keep separate catch blocks when the handling actually differs.
What dividing by zero actually does
Three operations, three different answers, and only two of them throw:
<?php
$zero = 0;
var_dump(@(10 / $zero)); // @ used only to keep the warning out of this demo
var_dump(@(0 / $zero));
try {
intdiv(10, $zero);
} catch (DivisionByZeroError $e) {
echo "intdiv: ", $e->getMessage(), "\n";
}
try {
echo 10 % $zero;
} catch (DivisionByZeroError $e) {
echo "modulo: ", $e->getMessage(), "\n";
}
//> float(INF)
//> float(NAN)
//> intdiv: Division by zero
//> modulo: Modulo by zero
| Operation | PHP 7.4 — this grader | PHP 8 |
|---|---|---|
intdiv($x, 0) | throws DivisionByZeroError | same |
$x % 0 | throws DivisionByZeroError, message Modulo by zero | same |
$x / 0 | warns, and the result is INF, -INF or NAN | throws DivisionByZeroError |
Written without the @, that first line also prints Warning: Division by zero. The warning is the only signal that INF is not a real answer — which is exactly why the next section tells you not to hide it. PHP 8 turned the whole case into a thrown DivisionByZeroError, and added fdiv() for the times you genuinely want IEEE infinity; fdiv() does not exist on this grader.
Error suppression and assert
@expressionsilences diagnostics. Avoid it. The demo above is the rare defensible use, and even there it is hiding the one warning PHP gave you.assert($condition, 'message')is disabled by default in production builds, so it can never be your validation.- Throw instead. An exception is loud, typed, and impossible to ignore by accident.
Common mistakes
- Catching
Exceptionto handle aTypeError—TypeErrorextendsError, notException. Catch\Throwablewhen you need both. - Throwing a string —
throw "oops";stops the program withError: Can only throw objects. - Suppressing with
@— a silent failure is a bug you will meet again much later, with no clue attached. - Empty catch blocks — at the very least, report what happened.
- Listing the general type first —
catch (ArithmeticError ...)beforecatch (DivisionByZeroError ...)swallows the specific case, because the first matching block wins andDivisionByZeroErroris anArithmeticError.
Your exercise
Safe Divide reads a count, then that many a b lines, and prints
intdiv($a, $b) for each — except that dividing by zero must print
error: divide by zero instead, and the whole run must end with
handled: <count> where the count includes every line, failed or not.
That last requirement is what finally is for: increment the counter there and
it runs whether the division succeeded or threw. Increment it at the end of
try instead and every zero divisor skips it silently.
The trap is catch order. intdiv(5, 0) throws DivisionByZeroError, which
extends ArithmeticError — so a catch (ArithmeticError $e) written first
catches the zero case too and prints the wrong message. List the specific type
first. (intdiv(PHP_INT_MIN, -1) is the case that genuinely needs the general
handler: it throws a plain ArithmeticError, message Division of PHP_INT_MIN by -1 is not an integer.)
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…