Skip to content
Lesson 16 of 17

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:

ClassUse it for
Exceptionthe generic base for your own error types
LogicExceptiona bug in the calling code
InvalidArgumentExceptionan argument that should never have been passed
RangeException, DomainExceptiona value outside what the function accepts
RuntimeExceptiona failure only detectable while running

Two more are raised by the engine rather than by you:

  • TypeErrorPHP 7.0+, thrown when an argument or a return value does not match a declared type. It works on this grader.
  • ValueErrorPHP 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') answers false here, so never write a catch for 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)
  • catch branches are tried most specific first. InvalidArgumentException extends LogicException, so a catch (LogicException $e) placed above would quietly swallow both.
  • finally runs on every path — clean return, caught exception, even a return from inside the try.
  • $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
OperationPHP 7.4 — this graderPHP 8
intdiv($x, 0)throws DivisionByZeroErrorsame
$x % 0throws DivisionByZeroError, message Modulo by zerosame
$x / 0warns, and the result is INF, -INF or NANthrows 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

  • @expression silences 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 Exception to handle a TypeErrorTypeError extends Error, not Exception. Catch \Throwable when you need both.
  • Throwing a stringthrow "oops"; stops the program with Error: 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 firstcatch (ArithmeticError ...) before catch (DivisionByZeroError ...) swallows the specific case, because the first matching block wins and DivisionByZeroError is an ArithmeticError.

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.)

Up nextCustom Exceptions and the HierarchyClasses, Inheritance, Errors

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…