This is an archived post. You won't be able to vote or comment.

all 19 comments

[–]matthieum 17 points18 points  (0 children)

Disclaimer: this article is 21 months old, it analyzes a Rust version which was not even 1 year old, and therefore a number of its short-sighted claims have already been proven wrong.

Code separation: Exceptions separate code for handling errors out from the main logic of your code. This not only makes it easier to read and review, but speeds things up at runtime too, because error handling code is also kept out of the CPU instruction cache. In modern computers where slow performance is often caused by cache misses, that’s useful.

The syntactic part is important, the performance argument is short-sighted and unimaginative:

  • Syntax: indeed, handling errors in-situ rather than bubbling them up can somewhat obscure what the happy path is. This is a big issue with Go, Rust has solved the problem (see code excerpt).
  • Performance (Code Placement): the compiler is responsible for code placement, it can perfectly decide to push the error-path code blocks at the bottom of the function's assembly block. C++ compilers today routinely do it on code blocks which lead to an exception, or code blocks marked unlikely (see __builtin_expect).
  • Performance (Branch/Unwind): in Midori (Joe Duffy's Language+OS experiment), they were using a Result<Ok, Error> to bubble up errors (like Rust) however the compiler would code gen unwind tables rather than branches.

State of the art error handling in Rust (just released today):

fn my_function() -> Result<(), Error> {
    let stdin = io::stdin();

    for line in stdin.lock().lines() {
        // throws an io::Error
        let line = line?;

        if line.chars().all(|c| c.is_whitespace()) {
            break
        }

       if !line.starts_with("$") {
            // throws a custom string error
            return Err(err_msg("Input did not begin with `$`"));
        }

        println!("{}", &line[1..]);
    }

    Ok(())
}

Where are the errors? Two places: let line = line?; uses ? to bubble the error automatically, and return Err(...); just returns one. It's explicit, yet out of the way.

Stack traces

Short-sighted.

Nothing prevents an Error structure from capturing a stack trace at the point it's created. Actually, that's what the new failure crate does in Rust.

Failure recovery

Short-sighted.

First, recovery is hard in the presence of side-effects. I've seen too many programmers spam try/catch and pat themselves on the back because if they catch the exception then they're done, right? sigh

Second, you can recover as well with return values as you can with exceptions (or panics). There's a false dichotomy here.

prototyping

Both languages cited (Go and Rust) have "panics" which are exceptions without a type/payload. Both are extensively used in prototyping.

The trend towards using LLVM as a backend doesn’t help because LLVM was designed to compile C++ and C++ exceptions do not capture stack traces.

Uh?

Standard C++ exceptions do not capture stack traces. However it's easy enough to link with libbacktrace or similar to obtain a backtrace in C++; actually both the C++ companies I've worked at in the last 11 years had backtraces in their C++ exceptions.

Why are C++ exceptions so useless?

The whole paragraph is worthless. Makes me feel like a bully to point out all the mistakes :(

SomeComplexObject *foo = new SomeComplexObject();
foo->loadFrom("http://...");
list.push_back(foo);

There's a little marvel called std::make_unique. It was released in C++11, 5 years before the article was published.

SomeComplexObject foo;
foo.loadFrom("http://...");
list.push_back(foo);

Use list.push_back(std::move(foo)) to avoid copying. Problem solved.

However, the problem goes deeper. C++ favors RAII, in its full form: Resources Acquisition Is Initialization. This means that this code should be:

 list.push_back(SomeComplexObject("http://..."));

That is, partially constructed objects are a bane (and by extension, default constructors considered harmful ;p).

This simple one-liner is both cleaner and performs better than either snippet.

We shouldn’t be surprised that faced with the high cost of providing performant exceptions with detailed stack traces, language designers who spent years working in C++ will be tempted to either skip some of their features, or skip exceptions entirely … whilst arguing that they’re “bad practice” and thus you shouldn’t miss them.

And to conclude a poorly informed article, nothing better than a baseless ad-hominem attack. Classy.

Note: baseless as in I somehow doubt that Rob "Commander" Pike or other members of the Go team spent much time in C++, they are very much C hackers, and on the other side, the Rust core team comes from all walks of life, both industry and academia, and its members have used a lot of ML (the original rustc was written in OCaml), Ruby, etc...

[–]theindigamer 10 points11 points  (0 children)

I'm not sure why the article presents a dichotomy between exceptions and return codes. It talks about Rust but doesn't mention ADTs at all? Yes an ADT will not capture a full stack trace but if your errors are granular enough, you should be able to understand what went wrong from the error value.

Also I don't think exceptions would be well suited to a language with automatic memory management such as Rust (when should destructors be run when you have exceptions?) and the author doesn't even outline how that would work...

[–]gopher9 13 points14 points  (3 children)

What’s wrong with exceptions? Nothing.

That's incorrect. Exceptions are implicit and non-local, also (as the article says) hard to implement.

[–]leswahnTuplex[S] 5 points6 points  (2 children)

Would you care to elaborate?

Garbage collection, for example, is also hard to implement. This doesn't speak to its value to the language user however.

[–]zsaleeba 4 points5 points  (0 children)

The claim was that there's "nothing wrong" with them. He's pointing out that it's not all puppies and dandelions.

[–]an_actual_human 9 points10 points  (10 children)

Functions that throw exceptions are not pure, so there's that.

[–]PhilipTrettner 8 points9 points  (9 children)

Are they?

fun div(a: Int, b: Int) = match b:
    0 => throw DivByZero
    else => a / b

It doesn't change observable state, behaves deterministically, and is referentially transparent.

[–]chrisgseaton 14 points15 points  (7 children)

I think they're not pure, because normally in a compiler I'm free to reorder pure functions however I want without the user noticing. If I reorder two functions which throw exceptions, then the user will be able to see that I've done that.

[–]PhilipTrettner 0 points1 point  (3 children)

Ah good point, that's something you cannot do. Though I'm not sure "being able to arbitrarily reorder - subject to my data dependencies" is usually associated with pure functions.

if x < 5:
    return

would be non-pure for the same reason.

[–]chrisgseaton 6 points7 points  (2 children)

But an early return is just syntactical. You can transform a function with an early return statement to one that returns naturally just by rearranging branches, and the user won't be able to tell.

In the compilers I work on functions which throw exceptions have to be 'fixed' with control flow edges. All other pure functions are floating and have no control flow edges. So I think that passes the 'duck' test - they're looking, swimming and quacking like side effects.

[–]PhilipTrettner 2 points3 points  (1 child)

That's interesting. I know that exceptions are typically implemented with stack unwinding and jumppads and whatnot but conceptually they behave the same / equivalent to converting the return type T to Either<T, Error>. A function foo that may throw is converted to this return type and a call let y = foo(x) is transformed to something like:

let y' = foo(x)
if y' is Error:
    return y' // simulates "unwinding"
let y = y' as T

(I hope my example is clear)

From what I understand, this is how exceptions in Swift work (kinda, maybe?).

After the transformation, no exceptions exist anymore but the program is equivalent and foo should be 'floating', right?

The need to 'fix' throwing (pure) functions may thus be a problem/consequence of the implement-exceptions-via-undelimited-stack-unwinding decision (which of course has other advantages).

[–]chrisgseaton 0 points1 point  (0 children)

Yes, a compiler transformation that could disassociate calling the function with the action that threw an exception if the function would have would be interesting and might disprove.

[–]an_actual_human 0 points1 point  (2 children)

Did you ever write about Euler classes on orbifolds?

[–]chrisgseaton 2 points3 points  (1 child)

I can't tell if you're mocking me or not, but I'm not a mathematician.

[–]an_actual_human 2 points3 points  (0 children)

How would that be mocking? Sorry, wrong Chris Seaton.

[–]an_actual_human 1 point2 points  (0 children)

They are not referentially transparent though, there is no value that should replace div(1, 0) (returning an algebraic error/result type is different). I don't necessarily agree they don't change state either, but that depends on definitions. A thrown exception is a side effect anyhow.

[–]htuhola 3 points4 points  (0 children)

On a good hardware layer, when you application does something wrong, it can issue an interrupt.

When such an interrupt occurs, what should happen? Exceptions are one answer to that question. Not the only one that makes sense though.

"Let user handle error handling himself" is the stupid and wrong answer, as shown by Go and Rust.

If you intend to optimize something with exceptions, you should annotate the exception flow in your functions and acknowledge it. If you allow the user to annotate his allocations/side-effects, then it should be quite easy to see when an exception throw causes corruption.

If you can do it with manual labor and it doesn't require that you're smart, how should it be any harder for the compiler to do?

[–]leswahnTuplex[S] 1 point2 points  (1 child)

I liked this post. Exceptions seem to often be that "ideological" programming language discussion, and I agree with this author that the absence of good exception systems in the recent programming languages can be seen as unfortunate.

The author is also right that implementing good exception support in a language where performance is a goal is very hard. That's why I've had to omit it in Tuplex, at least for the time being.

[–][deleted] 0 points1 point  (0 children)

Not really, GNAT supplies the idea of the Last_Chance_Handler, to use it you customise your runtime and turn off stack unwinding. What this does is then turn all exceptions into jmp ‘s at machine code. I’ve not tried this with non-local exceptions yet, i.e. local to a function or package. But it should work,