Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:127202 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by qa.php.net (Postfix) with ESMTPS id EC4DF1A00BC for ; Sun, 27 Apr 2025 18:07:32 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1745777115; bh=PdxHiSVj0V6FBmXF+U6fYCjRBYBod7RW41Ker0xxFDU=; h=Date:From:To:In-Reply-To:References:Subject:From; b=LbylIyUHpFBrp46v7IzKFlzIl7+sFIGt55Wup5CWTUuU6WhBN5VSyZyQ0TPetvW00 enNNHp9ES13zw0tAJBjojzr7iskDwkyCBCoM44ucvItdkU7qs65uHhr6vRuiUDu1U4 XqCjwJVEVmuWty7rY3Io2jyCrvBnysD+x1B4ChnXTA8+KA3TAa6rBCza9OD8jeGUME fiCAeH9b6Xa4iYMT9mBabp4vDeZEv5bmnX42AaRZTAp7iyl52vQFVz7jZHSNkVAGqX rruSmN29gDWvhnvt76T6mdHTy3zjwtMpPSmOinQQY7HZPWrS969hnNbMCSPowqAn0E 9b8DQuO2yWd5g== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 36B0518003B for ; Sun, 27 Apr 2025 18:05:13 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.0 (2022-12-13) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-2.8 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,RCVD_IN_DNSWL_LOW, SPF_HELO_PASS,SPF_NONE autolearn=no autolearn_force=no version=4.0.0 X-Spam-Virus: Error (Cannot connect to unix socket '/var/run/clamav/clamd.ctl': connect: Connection refused) X-Envelope-From: Received: from fhigh-a4-smtp.messagingengine.com (fhigh-a4-smtp.messagingengine.com [103.168.172.155]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Sun, 27 Apr 2025 18:05:12 +0000 (UTC) Received: from phl-compute-10.internal (phl-compute-10.phl.internal [10.202.2.50]) by mailfhigh.phl.internal (Postfix) with ESMTP id D52E111401D1 for ; Sun, 27 Apr 2025 14:07:29 -0400 (EDT) Received: from phl-imap-06 ([10.202.2.83]) by phl-compute-10.internal (MEProxy); Sun, 27 Apr 2025 14:07:29 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= garfieldtech.com; h=cc:content-transfer-encoding:content-type :content-type:date:date:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to; s=fm3; t=1745777249; x=1745863649; bh=r6+9S4lyffzP0wilLGqH+ pis6FG8lV/WGsEKfGoIzBM=; b=t8Xv1EqJCacmVKGqUHcWl0NrtJjczv/cb1ljX aOpPIpyL3rt44wRaWEzgQruqqvm7fD/xIYH3fjKksrYDWWqlKSddDBR9W/e6nKJ5 2fAqAdWwp4Sb6RNesMwCNVGsYiKqxcXhd0oEWp7d9awfMKooWz5syl9tonZJhq7A o4huLEeydgaMejhhSosx1mtCLlm0bENKvT0GDUx7J3P0F2kFkhloM79NWq32C2g+ 3qotW4E/S4dn2kRPKu88P8z5RoOVIqSMQqLUyUpbtYW8fTUJvDTPdtB9BbCvRRCB v4WwmU2kXUZxXnKhTi4tQ3t8dZDgndLKIW30fFXkS2+qG57xw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:content-type :content-type:date:date:feedback-id:feedback-id:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to:x-me-proxy:x-me-sender :x-me-sender:x-sasl-enc; s=fm3; t=1745777249; x=1745863649; bh=r 6+9S4lyffzP0wilLGqH+pis6FG8lV/WGsEKfGoIzBM=; b=DI9083KYzbhB1326D 7PkpNB9f+9AlUnAWhtE5p+4P/iMzungmhBJvWm0sVp0m4KVDO2PmvkEaZFadDCmm JWW8/wNSFlZowJrmAs00O2YmQ2ySlSzupMnHgzU3xBgsV9t+XFYJ3v3T9J0ZAlJJ dkSF26ojVcECxnvrvNox9gvjFCgJ5kVaG8UIdfLta6oR9pswWuDi7/2BCjEPEbjv 6EcdizCopuyGG/Hap3po26Mx8wOmc4mWX0KpTwMrKOECrW9yM3x6cYWsP5aO7au1 2kg2YbkQzgbLTW6AHNPvyPgKpOeH58ZQofIBx76BLNxe913MyOVptVW/kJfvUOHN nMQxg== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefvddrtddtgddvheekjeehucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdggtfgfnhhsuhgsshgtrhhisggv pdfurfetoffkrfgpnffqhgenuceurghilhhouhhtmecufedttdenucesvcftvggtihhpih gvnhhtshculddquddttddmnecujfgurhepofggfffhvffkjghfufgtgfesthejredtredt tdenucfhrhhomhepfdfnrghrrhihucfirghrfhhivghlugdfuceolhgrrhhrhiesghgrrh hfihgvlhguthgvtghhrdgtohhmqeenucggtffrrghtthgvrhhnpedugedvlefgueegheef jeetffduveeltefhfeegjeffffelgedttdevkeegkedugfenucevlhhushhtvghrufhiii gvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpehlrghrrhihsehgrghrfhhivghlught vggthhdrtghomhdpnhgspghrtghpthhtohepuddpmhhouggvpehsmhhtphhouhhtpdhrtg hpthhtohepihhnthgvrhhnrghlsheslhhishhtshdrphhhphdrnhgvth X-ME-Proxy: Feedback-ID: i8414410d:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id 880E329C0072; Sun, 27 Apr 2025 14:07:29 -0400 (EDT) X-Mailer: MessagingEngine.com Webmail Interface Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 X-ThreadId: T7e0f24752af1ea38 Date: Sun, 27 Apr 2025 13:06:53 -0500 To: "php internals" Message-ID: In-Reply-To: References: <39597a9c-6854-40c6-a529-32b2b178cb27@app.fastmail.com> Subject: Re: [PHP-DEV] Concept: Lightweight error channels Content-Type: text/plain Content-Transfer-Encoding: 7bit From: larry@garfieldtech.com ("Larry Garfield") I'm going to respond to points raised by several people together; I'm using Ed's message as a starting point but this is also i response to Niels, Rob, and Andreas. On Sun, Apr 27, 2025, at 3:16 AM, Edmond Dantes wrote: > Good afternoon, Larry. > > Looking at the comparison table, it seems that the two most important > differences are: > > 1. Backtrace consumes a lot of resources. Yes. And even if it can be made faster (as it looks like Niels is doing, which is great), it will never be as fast as an empty constructor and a return. That's the level I'm proposing. > 2. There is an explicit contract for exceptions thrown by a function. Yes. > 3. I didn't fully understand the point about the exception hierarchy, > but it seems important too. I somewhat glossed over this point, but let me expand on it here. Exceptions currently have a Java-inspired hierarchy. Everything MUST extend either Error or Exception. The available exceptions are spread across core and SPL, but internals is not supposed to use the SPL ones. For the use cases I'm talking about, "InvalidArgumentException" is the most common case, I expect. Or rather, special cases of that. However, it extends LogicException, which is specified in the docs as "Exception that represents error in the program logic." Which is... *wrong*, because half the time or more an InvalidArgumentException is a case of validating user data that cannot be fully validated by the type system... and thus not really a programmer error. It's only a programmer error if they don't handle it gracefully. Moreover, all exceptions currently track: * message * code * file * line * trace * previous exception Those are useful when they show up in logs read by a human. For the use cases I am describing, none of them are relevant, useful, or desireable. These values are for programmatic use only. But all of those are baked into the exception system at its core. Just having a third "kind" of exception (in addition to `extends Error` and `extends Exception`) would not really solve anything. > The issue with the missing contract could have been solved even for > exceptions, without creating a new entity. This is incorrect, as making the current exception system checked would be a ginormous BC break. And having some throwables be checked and some not, but using the same syntax and class hierarchy... well, that's the reason everyone detests Java's exceptions. Let's not do that. > Regarding Backtrace, the following questions seem fair: > > 1. What if I want to know where the situation occurred? Can I just > ignore this information? That is completely and utterly irrelevant, just as it is completely and utterly irrelevant on which line a `return` statement appeared. I think the framing of this concept as "lighter exceptions" is the wrong lens. I probably contributed to that in my initial explanation, so let me try and clarify: What I am proposing is not "Better exceptions." It's "a second kind of return value." It's closer to Rust's Result types or Go's multi-returns, but spelled differently. That is, it turns out (see the Error Model article previously), logically identical to checked exceptions. But if the mental model of "what is an exception" is what PHP has today, then that is more misleading than helpful. So let's not talk of that further. Rather, consider this example: class Repo { public function getUser(int $id): User { $record = $this->db->query("...")->fetchOne(); if ($record) { return new User(...$record); } // Uh, now what? } } There's various ways to handle that case. Sticking an exception at the end of the method is one option, but as I've pointed out, a particularly bad one. "That user isn't here" is not an error case that should be able to silently crash the application, just because someone put an invalid number in a URL. We could make the return value ?User and then return null, but now we have to manually check for null every frickin' time we call the method, or we get random null errors in who knows where. And it doesn't let us differentiate between "user not found" and "that is a negative int, which is never correct you idiot." Suppose a hypothetical world where we had generics: // We literally need *nothing* on these classes other than their type. // More complex cases might, but in this case, anything more than this is a waste. interface RepoErr {} class NoSuchUser implements RepoErr {} class NegativeId implements RepoErr {} class Repo { public function getUser(int $id): Result { if ($id <= 0) { return new Result::err(new NegativeId()); } $record = $this->db->query("...")->fetchOne(); if ($record) { return new Result::ok(new User(...$record)); } return new Result::err(new NoSuchUser()); } } And then to use it, you MUST do: function doStuff($id) { $ret = $repo->getUser($id); if ($user instanceof OK) { $user = $ret->value; } else { if ($ret->err instanceof NoSuchUser) { display_user_message('Who is that?'); return; } else if ($ret->err instanceof NegativeId) { display_user_message('Buddy, that's not a thing."); return; } } // If you got here, it means the getUser call way way way up there was valid. } I think it's reasonably obvious that is extremely clumsy, which is why almost no one in PHP does it, including FP stans like me. Plus that whole generics thing. What I've started doing is using union returns as a sort of "naked result": class Repo { public function getUser(int $id): User|UserErr { if ($id <= 0) { return new new NegativeId(); } $record = $this->db->query("...")->fetchOne(); if ($record) { return new User(...$record); } return new NoSuchUser(); } } function doStuff($id) { $user = $repo->getUser($id); if ($user instanceof UserErr) { ... } $user is now usable. } But that relies on me always remembering to do that check, and means static analysis tools can't know what the type of $user is until I check it. Better, still not great. What I am proposing is to take the Result type example and change the spelling to: class Repo { // We get reliable type information without generics! public function getUser(int $id): User raises UserErr { if ($id <= 0) { raise new NegativeId(); } $record = $this->db->query("...")->fetchOne(); if ($record) { return new User(...$record); } raise new NoSuchUser(); } } And then something reasonable on the receiving side, which I've not fully thought through yet. The whole point of this thread is "should I take the time to think that through?" :-) If we used try-catch or something structured the same, then we at least get: function doStuff($id) { try { $user = $repo->getUser($id); // Code that uses $user, which is guaranteed to be a User. return ...; } catch (NoSuchUser) { // Note not capturing the value, as we don't need it in this case. display_user_message('Who is that?'); } catch (NegativeId) { display_user_message('Buddy, that's not a thing."); } } I'm not convinced that's the right syntax, but it's a possible syntax. If you want to defer handling of a particular error to the caller, then you would explicitly do this: function doStuff($id): string raises UserErr { try { $user = $repo->getUser($id); // Code that uses $user, which is guaranteed to be a User. return ...; } catch (UserErr $e) { raise $e; } Which is why I think we do want some kind of syntax similar to Rust's ?, so the above could be shortened back to this: function doStuff($id): string raises UserErr { $user = $repo->getUser($id) reraise; // We have a good user. } If you try to raise an error from a function that doesn't specify it... that is exactly the same as trying to return an array from that function. `return array` would be a type error on the success channel. `raise new ProductErr` would be a type error on the failure channel. Same idea. Again, I don't think try-catch in its current form is ideal. I'm not sure what is. I'm trying to decide if it would be a waste of my time to figure out what would be better. But again, this is *not an exception*. This is a second return channel, aka a different way to spell a Result type, such that we don't need a clumsy Result type. --Larry Garfield