Rust has no Exceptions?

I am learning Rust.

There are ‘panics’, and there are well known functions with which you can shoot yourself in the foot with them…

But apparently idiomatic code more or less assumes reliable type definitions… I have to say this is v nice, and something I wish Haskell put (even) more attention on.

In this small aspect I think Rust (being modern) is ahead of Haskell (maybe I should switch to a better Prelude : ). And for beginners a big win, imo.

2 Likes

Why better? I think it’s significantly worse. Naturally, untracked exceptions in naked IO are a pain in Haskell, but we now have IO-based effect systems (Bluefin and effectful) that can track native IO exceptions in the type system. The best of both worlds!

6 Likes

Wait until you need to work in a non trivial codebase and you’ll see the amount of work required to manage errors as values. I’ve been there and I much rather work with exceptions.

5 Likes

“Errors as values” is something like Either FailReason a right?
I am curious, what is wrong with it?

2 Likes

to arrive at a problem you need to assume 2 reasonable things

  1. a majority of actions can potentially fail and
  2. they can fail differently

1 forces you to use the Monad instance of Either or create a case pyramid and live your life on indentation level 20
2 prohibits you from using the monad instance without a lot of wrapping/unwrapping

so after this you either use exceptions(which are baked in the language and hence free) or polymorphic variants(we don’t have language support like OCAML but there are libraries)

3 Likes

Bear in mind that the fanciest type systems sometimes lie to you. Things can fail where you don’t expect them to be able to fail. Try:

{-# LANGUAGE MagicHash #-}
import GHC.Prim
import GHC.Types
let (I# x) = 1 in let (I# z) = 0 in I# (quotInt# x z)

Background: x, z and quotInt# x z are members of an unlifted type Int# which is said not to contain bottom.

sorry it isn’t clear to me how this is related to my comment, when looking at the primops(which are all internal) they clearly specify which can fail with a unchecked exception. sure you can craft a malicious value that crashes the runtime system but that isn’t the game, programming and science in general is cooperative.
your comment is the equivalent of interrupting someone discussing legal minutia to enlighten them about the many uses of a gun.

1 Like

Two possible ways in which exceptions might improve upon errors-as-values for certain cases:

  • Exceptions make it easier for layers of code to communicate without the layers inbetween knowing about it. Two articles:

  • For Haskell in particular, asynchronous exceptions are at the base of useful libraries like async. Concurrency in an hypothetical Haskell that didn’t have async exceptions would look very different.

4 Likes

I don’t find this one very compelling. If you have the ability to abstract over effects, and hide internals, then it’s a non-problem. You can do it easily with monad transformers, for example

newtype MyMonadT m a =
    MkMyMonadT (ExceptT Foo m a)

and then no one in between the thrower and the catcher (without access to MyMonadT's definition) can intercept the exception. The story is even better in proper effect systems.

7 Likes

Pardon my ignorance, but how do you deal with asynchronous exceptions when you don’t have exceptions?

I know e.g. with Unix processes you can have signals and signal handlers. That’s just another color for the bikeshed, though. There’s always got to be some way to handle real, no-shit exceptions that break the IO abstraction barrier. If not exceptions as we currently have them, wouldn’t you still need some mechanism that would end up with a slightly different set of tradeoffs?

6 Likes

I think there’s room for both. Haskell lets you do both. Failures as return values is nice in some cases like parsers and whatnot. Failures as exceptions is nice when you’ve got a load of I/O based code that can fail in myriad ways and you don’t care to enumerate them nor would benefit in doing so.

At first I liked Rust’s angle until I saw people using it at work and saw that it’s actually an inconvenience: people sprinkle “anyhow” everywhere as a way to avoid having to produce a well typed error, and every function is littered with “?” on almost every line to the point of meaninglessness. Java has checked exceptions, and not everybody is too happy about them.

Haskell lets you be blasé like Erlang (throw and forget) or meticulous like Java/Rust, depending on your discretion. It’s not perfect but I feel lucky to have it.

MzScheme differentiated exceptions from termination of a thread by a custodian (Custodians) that couldn’t be stopped, IIRC. That was really neat. And Common Lisp folk use restarts (Beyond Exception Handling: Conditions and Restarts) all the time to provide a developer experience, which is another island of behavior quite far away from Haskell/Rust.

18 Likes

Pardon my ignorance, but how do you deal with asynchronous exceptions when you don’t have exceptions?

That’s the thing, Rust (and Go) only pretend they don’t have exceptions. Rust has panic and catch_unwind, Go has panic and recover.

I consider this pretense to be a massive flaw of these languages.

I agree with @chrisdone, sometimes you want unchecked exceptions, sometimes you want to track them in types. It generally depends on the use case and it’s nice that Haskell lets you do both. The problem with this is that it’s subtle and IME people don’t like this answer, they expect to be given a simple solution to a complex problem and both Rust and Go pretend they have it, but in practice it results in tons of noise in the code from manual passing of errors downstream everywhere (it’s a hot mess in Go in particular, at least Rust has some syntactic sugar for it).

10 Likes

Matt Parsons has a nice blog post describing the main pain point when no exception mechanism exists (or its use is verboten in a given code base): The Trouble with Typed Errors

To make typed errors work well (i.e. compose) you really need open variants. Unfortunately it’s a rare feature, and bringing it to haskell is hard.

1 Like

My main gripe with exceptions that are not tracked in the type is that they make it more complicated to include linear logic features. If you have a file handle or channel that needs to be closed, or memory that needs to be freed, then we want to guarantee that this happens in every code path. And the possibility of untracked exceptions makes this more difficult. This is a problem for both Haskell and Rust. Nikos describes the problem for Rust in Must move types · baby steps and Arnaud for Linear Haskell here: On linear types and exceptions

4 Likes

I think this is a misconception. Effect systems can compose exception types without open variants by (something isomorphic to) passing them in as arguments. For example, to get the “sum” of errors Foo and Bar you can use in Bluefin

(e1 :> es, e2 :> es) =>
Exception Foo e1 ->
Exception Bar e2 ->
Eff es r

or in effectful

(Error Foo :> es, Error Bar :> es) =>
Eff es r

This is perfectly composable, no new type system features necessary.

4 Likes

This is independent of whether they’re tracked in the type, isn’t it? For example, you can consider IO as ExceptT SomeException UIO, where UIO is a monad that doesn’t throw exceptions. I think the problem is having any throw operation where you can write

do
  r <- acquire
  throw MyException
  release r

and abort the execution of the block before release. This is so regardless of whether the do block is running in IO, ExceptT MyException SomeOtherMonad, or anything else.

1 Like

Tbh I have yet to try that out in earnest. I recall using a similar feature in the past that also relied on constraints (I think MonadError + classy prisms). But I found it difficult to use at the time.

Your example looks intriguing, thanks!

We probably mean the same thing, I was just too concise in my comment :slight_smile: I am not too worried about a throw operation that just crashes the program if some invariant has been broken, but only about throw operations that can be handled, since only these introduce a new control flow mechanism that can break linear logic reasoning. Through your work on bluefin I think you probably agree that exceptions should be handled lexically (i.e. the handler responsible for catching the exception should be determined by lexical scope, not dynamic scope), so a sensible throw operation needs to also be passed a capability, and this capability/continuation must be tracked in the type system, which makes it possible again to use linear logic reasoning.

3 Likes

there are a ton of open variants libraries one of them is vary sure i guess it isn’t as nice as if it were a language feature but it certainly works.

2 Likes

Indeed, I am also aware of variant (ghcup uses it, for what it’s worth). I have always balked at the (perceived?) complexity, however.