DEV Community

Cover image for Have you used Rust (or Go, or anything else) and just felt relieved by how they treat errors?
Daniel Reis
Daniel Reis

Posted on

23 5 5 5 4

Have you used Rust (or Go, or anything else) and just felt relieved by how they treat errors?

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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.

Quadratic AI

Quadratic AI – The Spreadsheet with AI, Code, and Connections

  • AI-Powered Insights: Ask questions in plain English and get instant visualizations
  • Multi-Language Support: Seamlessly switch between Python, SQL, and JavaScript in one workspace
  • Zero Setup Required: Connect to databases or drag-and-drop files straight from your browser
  • Live Collaboration: Work together in real-time, no matter where your team is located
  • Beyond Formulas: Tackle complex analysis that traditional spreadsheets can't handle

Get started for free.

Watch The Demo 📊✨

Top comments (10)

Collapse
 
xwero profile image
david duymelinck • Edited

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.

class validator 
{
    public function validate() : true|InvalidFields
    {
        // return true or add one or more messages to the InvalidFields object.
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
lucasayabe profile image
lucas-ayabe

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:

abstract class Result
{
    public static function ok($value): self
    {
        return new Ok($value);
    }

    public static function err($value): self
    {
        return new Err($value);
    }

    abstract function isOk(): bool;

    public function isErr(): bool
    {
        return !this->isOk();
    }

    abstract function fold(callable $ok, callable $err): mixed;
}

class Ok extends Result
{
    public function __construct(private mixed $value) {}

    public function isOk(): bool
    {
        return true;
    }

    public function fold(callable $ok, callable $err): mixed
    {
        return $ok($this->value);
    }
}

class Err extends Result
{
    public function __construct(private mixed $error) {}

    public function isOk(): bool
    {
        return false;
    }

    public function fold(callable $ok, callable $err): mixed
    {
        return $err($this->error);
    }
}

function div(int $x, int $y): Result
{
    if ($y === 0) {
        return Result::err("Cannot divide by 0");
    }

    return Result::ok($x / $y);
}

div(1, 0)->match(
    err: fn (string $message) => print($message),
    ok: fn ($value) => print($value)
);
Enter fullscreen mode Exit fullscreen mode

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

Collapse
 
xwero profile image
david duymelinck

I was confused by div(1, 0)->match( I think this should be div(1, 0)->fold(.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

Personally, I quite like how zig does errors

Collapse
 
plutonium239 profile image
plutonium239

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

Collapse
 
luqmanshaban profile image
Luqman Shaban • Edited

For all the languages I’ve tried out, Go, by far has the best error handling

Collapse
 
shricodev profile image
Shrijal Acharya

+1

Collapse
 
phil_coder_fbe725ee08b389 profile image
Phil Coder

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.

Collapse
 
zaunere profile image
hz

Very much in favor of a new engine for PHP.

Collapse
 
peixotons profile image
Gabriel Peixoto

Really awesome Daniel

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay