Errors, and checking what arrives
Throw when a value cannot be used, catch where something can be done about it, and validate at the edge so the inside of the program only ever sees good data.
An exception is a refusal, with a reason
A function that cannot do its job has three options: return something wrong, return false and hope the caller checks, or throw and stop the normal path. Throwing is the honest one — throw new InvalidArgumentException('Email is not valid.') — because it cannot be ignored by accident, and it carries its reason in the message.
The exception travels up through every function that did not catch it, until something does. That is the design: the code that knows a value is wrong rarely knows what to do about it, and the code that can decide — skip the row, ask again, show the user — is usually far above.
Catch what you can handle
try { … } catch (InvalidArgumentException $error) { … } runs the risky part and takes over when the named kind of exception arrives; the object carries getMessage() and the line it came from. Catching by the narrowest type that describes what you expect keeps real bugs visible — a blind catch (Exception) swallows everything, including the mistakes you need to see.
finally runs either way, whether the try succeeded, threw or returned — the place for the cleanup that must happen regardless. And nothing is caught by accident: an exception with no handler stops the script with the message and the line, which is exactly right while developing.
A custom exception class is how a program says what went wrong in its own vocabulary. class ScoreError extends InvalidArgumentException {} costs one line and lets a caller catch the exact failure it can handle, while a different InvalidArgumentException from somewhere else still travels. The class name becomes part of the contract, and the message stays for the reader.
Validate at the edge
The best time to refuse a value is before it enters the program. Validation functions at the edge each answer one question and either return a clean value or throw: is the name non-empty, is the email an email, is the age a whole number in range. Inside, the rest of the code uses the clean value and never checks again — the checks are in one place, and the body of the program reads without guards.
PHP ships the validators for the common cases: filter_var($email, FILTER_VALIDATE_EMAIL) answers the email question the way the standard library understands it, and filter_var($age, FILTER_VALIDATE_INT) the integer one. Rolling your own email check with a regular expression is a famous way to build something worse than the one that already exists.
Validation is per value, but the decision is often per record. A name can be valid and an email invalid in the same row, and the row is rejected as a whole. That is why the validating functions return a clean value and throw, rather than returning a boolean: the caller can stop at the first bad field, or collect every message, without the functions knowing which style the form uses.
Validate, throw, catch, clean up
<?php
declare(strict_types=1);
function validateName(string $name): string
{
$clean = trim($name);
if ($clean === '') {
throw new InvalidArgumentException('Name is required.');
}
if (strlen($clean) < 2) {
throw new InvalidArgumentException('Name is too short.');
}
return $clean;
}
function validateEmail(string $email): string
{
$clean = trim($email);
if (filter_var($clean, FILTER_VALIDATE_EMAIL) === false) {
throw new InvalidArgumentException('That is not an email address: ' . $clean);
}
return $clean;
}
$attempts = [' ', 'A', '[email protected]', 'not-an-email'];
foreach ($attempts as $attempt) {
try {
$name = validateName($attempt);
echo 'Accepted name: ' . $name . PHP_EOL;
} catch (InvalidArgumentException $error) {
echo 'Rejected: ' . $error->getMessage() . PHP_EOL;
} finally {
echo ' (checked)' . PHP_EOL;
}
}
try {
validateEmail($attempts[3]);
} catch (InvalidArgumentException $error) {
echo 'Email problem: ' . $error->getMessage() . PHP_EOL;
}A signup that refuses politely
Run it and read which rows pass. Then add an age rule — a whole number between 13 and 120, checked with filter_var — and let the loop reject the rows that fail it.
A caught exception that is only ignored is a bug wearing a disguise. If nothing can be done about it, let it travel — a loud stop with a message beats a silent null three steps later.
The exception object, and the edge
An exception is an object, not a special return value. It carries a message, a code, the file and line where it was thrown, and a stack trace. throw stops the current path and unwinds through every call until a catch names a matching type; if none does, the script stops with that information. Exception and Error both implement Throwable, which is the type to catch only at the very top of a program.
Validation belongs where data enters, and the standard filters do most of the work. filter_var($value, FILTER_VALIDATE_INT, ["options" => ["min_range" => 1, "max_range" => 10]]) returns the integer or false. The comparison must be === false, because 0 is a valid integer and is falsy; a loose check would reject a legitimate zero.
Three exception mistakes make bugs harder to see: catching Exception around everything, catching and doing nothing, and treating filter_var()’s false with ==. Catch the narrow type you can handle, let the rest travel, and compare the filter result with === false so zero stays valid.
Try these next
- Validate an age between 13 and 120 with
filter_var()and the range options. - Write a
ValidationExceptionand catch only that class in a loop. - Count accepted and rejected rows, and print a separator in
finallyon every pass.
A score sheet that refuses bad rows
Run the sheet, then add a row with the score `0` and confirm it is accepted rather than rejected. Add a bonus score of `95.5` and watch the integer filter refuse it.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why throw instead of returning false from a validating function?
