Alright, I need to vent. I've reflecting about engineering recently I swear, exception handling in PHP feels like duct taping over a leaking pipe.
Like, why is everything so optional? You can throw exceptions, sure, but half the time you're just praying that some function doesn't silently fail or spit out false without context.
Meanwhile, I’ve been playing with Rust and Go lately and bro... it’s like, it feels safer (?). Every error is explicit. Either it worked (Ok) or it didn’t (Err), and you have to handle it. You don’t get to ignore it and pretend everything’s fine. The compiler literally says: "Nope, go back and deal with it."
But I feel that do that in PHP would be an Anti-Pattern but at the same time would be safer to work with this type of "Result Objects".
With Exceptions
function getUser(int $id): array {
// Simulate DB access or something that might throw
if ($id <= 0) {
throw new InvalidArgumentException("Invalid user ID");
}
return ['id' => $id, 'name' => 'Daniel'];
}
try {
$user = getUser(0); // ⚠️ This will throw
echo "User name: " . $user['name'];
} catch (InvalidArgumentException $e) {
echo "Caught exception: " . $e->getMessage();
} catch (Exception $e) {
echo "Something went wrong: " . $e->getMessage();
}
With a Result Class
class Result {
private function __construct(
public bool $isOk,
public mixed $value = null,
public mixed $error = null
) {}
public static function ok(mixed $value): self {
return new self(true, $value);
}
public static function err(mixed $error): self {
return new self(false, null, $error);
}
public function isOk(): bool {
return $this->isOk;
}
public function unwrap() {
if (!$this->isOk) {
throw new RuntimeException("Tried to unwrap an Err: {$this->error}");
}
return $this->value;
}
public function unwrapOr(mixed $fallback): mixed {
return $this->isOk ? $this->value : $fallback;
}
public function getError(): mixed {
return $this->error;
}
}
// --------------
function getUser(int $id): Result {
if ($id <= 0) {
return Result::err("Invalid user ID");
}
return Result::ok(['id' => $id, 'name' => 'Daniel']);
}
$result = getUser(0);
if ($result->isOk()) {
$user = $result->unwrap();
echo "User name: " . $user['name'];
} else {
echo "Error: " . $result->getError();
}
I know that the "lack" of generics (annotations solve this partially) makes it harder to accomplish, but I mean, this would improve the whole ecosystem IMHO.
Also I just started to think about that because recently Microsoft started the revamp/reload/renew of typescript in go and I was wondering: what would be if PHP got a new engine (since Zend Engine is a total mess) with a couple of new features? Would be amazing.
Top comments (10)
With great power comes great responsibility.
If thrown exceptions go lost in the application, it is most of the times a deliberate action by a developer. Php is not the problem.
I think the
Result
class is fine if you choose the monads route. Another option is to use multiple return types.I would use those options if the warnings are going to picked up by the application.
The power of choosing names for the exceptions makes them more understandable. And that also makes it clear for developers why the exception is thrown.
Only having a RuntimeException when something goes wrong is not enough.
at this point you should just return a tuple (like Go and Python do), there's no value to have a class that is a giant DTO with some predicate methods.
I really like how Result makes error handling better by using the power of monads to do the trick, but this implementation isn't a monad, and its bad.
You don't need the isOk attribute since it let you introduce invariants on your code, what happens if you have an error and isOk is true? I know that the custom constructors ensure that this won't happen, but it wasn't needed have this flaw there to have the extra care to prevent after.
It would've be better if you checked if there's a value in error or in value inside the isOk method to have this info, and even better than that if you made the Result an abstract class and implemented Ok and Err as subclasses since you would need to only one attribute instead of 2 making any ambiguity in the code non-existent.
Having an unwrap method makes so that this code is as bad as using exceptions, if not worse, since you can just write an if ($result->isOk()) ignore the else, and never handle the error, being literally the same as one exception, with the added drawback that at least try catch lets you catch the exceptions by type, here the errors are strings so you would need to have a lot of constants to be checked inside an switch/match to handle different types of errors if you ever compose result values (it can be fixed by using enums as errors, but with try catch you already has this out-the-box).
A better implementation would be a fold method instead of an unwrap, ex:
This actually emulates how Rust and the functional approach requires you to handle the error, you can't get the value from inside the Result without doing pattern matching (in this case emulated through a method).
I was confused by
div(1, 0)->match(
I think this should bediv(1, 0)->fold(
.Personally, I quite like how zig does errors
The "errors are values" paradigm grew most with go iirc, I'm sure it's not the first language to do so but definitely one of the biggest
For all the languages I’ve tried out, Go, by far has the best error handling
+1
I'm old enough to remember when PHP was hot and ruled the web and a lot of app dev for a while. I also remember moving a major company off PHP (for one system) to Go. I'm curious as to why anyone hangs around on PHP these days.
When I came out of college it was the cheapest way to get going. Now you have all this free and open language tooling that is far safer. Why add to the massive amount of bolting on that has to be done to make it better? I'd say it's time to let PHP and Java die. This is from someone who was once like a PHP guru.
Very much in favor of a new engine for PHP.
Really awesome Daniel