Skip to content
Custom Exceptions and the Hierarchy
step 1/7

Reading — step 1 of 7

Learn

~4 min readClasses, Inheritance, Errors

Custom Exceptions and the Hierarchy

throw new Exception('bad input') works, but it tells the catcher nothing it can act on — every failure arrives as the same type, so the only way to tell them apart is to read the message string and hope nobody rewords it.

Your own exception classes fix that: the type carries the meaning, so a caller can catch exactly the failure it knows how to handle and let the rest keep travelling. To place those classes correctly you need PHP's built-in hierarchy, which is where this lesson ends.

Custom exception classes

<?php
class ValidationException extends Exception {
    private string $field;

    public function __construct(string $field, string $message) {
        parent::__construct($message);
        $this->field = $field;
    }

    public function getField(): string {
        return $this->field;
    }
}

try {
    throw new ValidationException('email', 'must contain @');
} catch (ValidationException $e) {
    echo "field ", $e->getField(), ": ", $e->getMessage(), "\n";
}

//> field email: must contain @

Two things make that work. extends Exception places your class inside the hierarchy, so a catch (Exception $e) further out still finds it. And parent::__construct($message) is what gives getMessage() something to return — write your own constructor without that call and getMessage() hands back string(0) "".

Grader note: modern PHP compresses that constructor with 8.0 property promotion, often adding 8.1's readonly. Both are parse errors on this course's PHP 7.4 grader, and the file will not run at all. Declare the property in the class body and assign it, exactly as above.

Most of the time you need far less. A body-less marker class is already a complete, useful exception:

<?php
class AgeException extends Exception {}

try {
    throw new AgeException('not a number');
} catch (AgeException $e) {
    echo $e->getMessage(), "\n";
}

//> not a number

It inherits Exception's constructor, so the message still works — and that is precisely the shape your exercise needs.

The exception hierarchy

PHP 7 unified errors and exceptions. Everything throwable implements \Throwable:

Throwable
├─ Error                 (raised by the engine)
│  ├─ TypeError                (PHP 7.0)
│  ├─ ValueError               (PHP 8.0 — absent on this grader)
│  ├─ ParseError
│  └─ ArithmeticError
│     └─ DivisionByZeroError
└─ Exception             (your application's errors)
   ├─ LogicException
   │  ├─ InvalidArgumentException
   │  └─ DomainException
   └─ RuntimeException
      ├─ OutOfBoundsException
      └─ UnexpectedValueException

The split bites the first time an engine error slips past a catch:

<?php
function needInt(int $n): int { return $n; }

try {
    needInt([]);
} catch (Exception $e) {
    echo "caught as Exception\n";
} catch (Throwable $t) {
    echo "only Throwable caught it: ", get_class($t), "\n";
}

//> only Throwable caught it: TypeError

catch (Exception $e) handles your application's failures. catch (\Throwable $e) handles those and the engine's. Wrapping everything in Throwable hides bugs; using it once at the very top of a program is reasonable.

Common mistakes

  • Losing the message in a custom constructor — if you write __construct, call parent::__construct($message) or getMessage() comes back empty.
  • Extending Error for an application failureError is the engine's half of the hierarchy. Your classes belong under Exception.
  • Forgetting the leading \Exception resolves fine at global scope; inside a namespace write \Exception or import it with use.
  • Catching Exception and expecting to see a TypeError — that one is an Error. Only \Throwable covers both halves.

Your exercise

Validate Age wants class AgeException extends \Exception {} and a validateAge(string $s): int that throws an AgeException carrying the message empty, not a number, or negative, and otherwise returns the number.

The mistake the grader catches is the message text: the tests compare error: not a number byte for byte, so not numeric, a capital letter, or appending the offending input all fail. The second is check order — test $s === '' before is_numeric($s), because the hidden empty-input case expects error: empty while is_numeric('') is already false, so the wrong order reports not a number there instead. Cast with (int) only once the numeric test has passed, print age: 42 on success and error: <message> on failure, and close both lines with "\n". The starter's try / catch (\Exception $e) frame is already written; uncomment its lines once your function exists.

Discussion

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

Sign in to post a comment or reply.

Loading…