Explanation of the Error handling

Hi all. Well, I glad to see how young and brave web programmers trying to create a language suitable for system programming. It's looked like an alternative evolution. Not bad for me, may be we will find a new good ideas. But I am old enough and I want give some historic explanation about the reasons of the error handling in the old languages. Looked like the authors of GoLang and Rust is too young and don't know it. At very ancients times the program languages operated with addresses of commands. Look at example, I believe (but not sure) that it was able be runned on ZX Spectrum.

10 LET i=1
20 GOTO 40
30 PRINT ",";
40 PRINT i;
50 i=i+1
60 IF i<5 THEN 30
70 PRINT

BTW, with such paradigm special operators for "while loops" were not needed. Well, very ugly code and structural program languages were raised, where programmer operates with not addresses, but with blocks of code. Language C, for instance, was planned be a truly structural language, but it keep GOTO operator just for unknown cases. And, after of years of using C, revealed for what GOTO is still needed. And this still prevented C to be true structural language. GOTO was needed only for error handling. Look at very simple example on C.

#include <stdio.h> 
int main(void)
{
    int rc = puts("Hello World");
 
    if (rc == EOF)
       perror("fputs()"); // POSIX requires that errno is set
}

But what to do, if we want to write many different strings. Well, will be a lot of puts() functions, but we do not need to do error handling block separately for each of them. Error from the any fputs() function may be processed in the same manner. Thats why GOTO was needed.

#include <stdio.h> 
int main(void)
{
    int rc;
    if (EOF==puts("Say: A"))
       goto error_handling;
    if (EOF==puts("Say: 'B'"))
       goto error_handling;
    if (EOF==puts("Say: 'C'"))
       goto error_handling;
    if (EOF==puts("Say: 'D'"))
       goto error_handling;
    if (EOF==puts("Say: 'E'"))
       goto error_handling;
    return 0;
    error_handling:
       perror("puts()"); // POSIX requires that errno is set
       //to do some complex error handling
}

Thats why Exception was invented in almost all program languages and due to this GOTO was forgotten. And ancient languages became true structural. Well, was very funny for me to see that in GoLang exceptions were abscent. And now lets discuss what exists in Rust. https://doc.rust-lang.org/book/ch09-00-error-handling.html Well, exists macro panic!() and special return type Result<T, E>. The first looked like exit(1) in C, while the second in C like returning error code of function in the static variable errno. Nothing new. There is not "exceptions", but what about the code pattern, that I wrote above? Is in Rust something like (fantastic language): BEGIN puts("Say: 'A'") puts("Say: 'B'") puts("Say: 'C'") puts("Say: 'D'") puts("Say: 'E'") EXCEPTION perror("puts()"); // or other, more complex, routine END Well, something exists in the Recoverable Errors with Result - The Rust Programming Language To do something to simulate such pattern in Rust need to add "puts()" into the separate internal function, use operator ? at the end of all such functions and do exception handling in the external function. This is not fun at all and looked like ugliness. First of all, syntactic sugar operator ? can work with Result return type or with Option return type, but not with both. This is not good. Trivial example is the function, that searches substring in a string. What to do if such function don't find substring? Return None or Error? Of cause None, because this is not an error. Thats why I like Apple Swift and dislike Python. BTW, Rust is good in this too. But what about if we need search a file for a string? Such function must return None, if not found and Error if there will be errors in reading of the file. So both variants is needed. This can be easily made by encapsulate Option inside Ok of Result. But syntax sugar operator ? must support such use case too. Second, this is not fun to create unnecessary function only to emulate exception handling. I don't know real reason, why you didn't make "'exceptions". May be was good reason. But, at least there must be example in the Chapter " Error Handling" how to emulate the simple and transparent block of code with exception handling (example above) from other languages inside Rust. May be there is possible to create an anonymous function (closure) with external error handling to simulate the habitual block of code with section for the exception handling? Will be such example too complex or badly readable? Is this good for programmers and Rust?

This reads like it might be a better fit for the users forum since it's about using Rust, but it's a bit of a gray area since it's also about the design principles motivating Rust's design.

This is accurate enough; panics are intended to be used for programmer error and/or unexpected and unrecoverable program state. In the default build configuration panics will unwind the thread instead of immediately exiting, and an unwind can be caught, but a panic can also cause an abort due to any one of a number of potential reasons, so SHOULD NOT be used for intended program flow control.

It is a currently unstable feature to write try { /* code */ } blocks which act as a "catch" scope for ? based control flow breaks. So the most direct port of your example routine would be something like

// more idiomatically typed Rust wrappers
fn puts(s: &CStr) -> Result<(), io::Error>;
fn perror(s: Option<&CStr>);

fn main() {
    let _ = try {
        puts(c"Say: A")?;
        puts(c"Say: B")?;
        puts(c"Say: C")?;
        puts(c"Say: D")?;
        puts(c"Say: E")?;
        return;
    };
    perror(Some(c"puts()"));
}

That type inference fails and this example doesn't compile as-is is a significant part of the reason that try is still unstable; we are still working on finding a formulation that works better in more cases.

Although it's also worth noting that the way you'd actually write this in kinda idiomatic Rust[1] would instead be something more like:

fn main() -> io::Result<()> {
    let o = &mut io::stdout();
    writeln!(o, "Say: A")?;
    writeln!(o, "Say: B")?;
    writeln!(o, "Say: C")?;
    writeln!(o, "Say: D")?;
    writeln!(o, "Say: E")?;
    Ok(())
}

That's right, you can directly return Result from main and the language runtime will handle converting that into an appropriate message and exit code for you.

Going from Result to Option is trivial — it's just .ok()?. Going from Option to Result is less trivial, since you have to choose what error value to provide, but it's still straightforward — for example, .ok_or_else(io::Error::last_os_error)?.

Why? You still have return available to you, and certainly any such routine is a meaningful enough chunk of functionality to deserve a function name.

task::Poll<Result<_, _>> actually does something kind of like this, supporting ? yeeting the Result::Err from Result<_, _>, from Poll<Result<_, _>>, and from Poll<Option<Result<_, _>>>. This is generally (although not universally) regarded as an unfortunate mistake, and that the semantics of ready! would have been the better behavior.

Rust isn't afraid to ask you to clarify what you mean and do some "boilerplate" type conversions. This is why arithmetic requires all numbers to be the same type instead of doing promotion, for example. If you use ?, it ideally shouldn't be too ambiguous whether it's yeeting Result::Err or yeeting Option::None.

goto fail isn't exactly simple. Sure, it is, when used correctly, useful structured control flow, but it's far to easy to get accidentally wrong. That's why Drop-based end-of-scope cleanup is generally preferable.

There are borrow lifetime reasons why Drop isn't an ideal solution in all cases, thus why you might see discussion around adding a native defer capability to Rust, but it works wonderfully well in most cases.

Using drop-based cleanup, which fires both in the case of early return (via ?, return, or otherwise) and panic! unwind, and using a helper for anonymous closure-based impl Drop like from the scopeguard (more popular, 244M) or defer (less popular, 486K) crates:

let guard = defer(|| perror(Some(c"puts()")));
puts(c"Say: A")?;
puts(c"Say: B")?;
puts(c"Say: C")?;
puts(c"Say: D")?;
puts(c"Say: E")?;
mem::forget(guard); // "forget" to run the cleanup

Yes, this is also relatively common, e.g. perhaps you might write something like:

let Ok(_) = (|| {
    puts(c"Say: A")?;
    puts(c"Say: B")?;
    puts(c"Say: C")?;
    puts(c"Say: D")?;
    puts(c"Say: E")?;
    Ok::<(), io::Error>(())
})() else {
    perror(Some(c"puts()"));
};

although again you'll often encounter type inference failures and need to annotate some types to help it out, since the signature of closures is inferred and ? is actually extremely generic, relying on functions' fully specified signatures to guide type inference. (This is exactly the inference problem blocking try.)

Monadic error handling with Result treating errors as values is very good for Rust. Actually handling errors can be annoying, certainly, but that's mostly intrinsic cost of addressing the failure case and not incidental costs of the paradigm used.


  1. Most Rust snippets will actually use println! and panic on an IO error writing to stdout. (Except that a missing stdout silently drops writes.) Robust Rust programs sometimes try to avoid this, but just assuming that stdout and stderr work is the default. ↩ī¸Ž

5 Likes

You wrote it all on one line. You need 2 newlines for a line break (or a \)

Nit: None of the original developers of Rust were primarily web programmers. If you look at the early conversations of Rust, they largely involved people who were experienced in both (OCaml or Haskell) and C++, generally with a solid background in programming language theory.

I can answer some of that as I was part of these conversations.

The main objective with error-handling in Rust was to have static guarantees that all error cases (that did not involve an immediate process kill) were either handled or propagated, without losing performance. As demonstrated by Java, this is possible with PL/I-style exceptions, but going in this direction meant complicating the type system a lot. As shown by OCaml or Haskell, you can have the same expressive power without the complication, although (in some cases) at the expense of performance.

As it turned out, using Result<T, E> and early return resulted in a simpler language, a simpler compiler, and (once we introduced the try! macro, now known as the ? operator) something just as readable in (almost) all cases, and no loss of performance.

So the design of Rust went with Result<T, E>, early return and try!/?.

7 Likes

So, counterpoint to this is Microsoft Research Project Midori (Joe Duffy - The Error Model). They had a surface language syntax that was generic over the style of propagationn. So they tried both exceptions and result style error handling as the underlying implementation, and for their use case (happy path is much more common) exceptions was a performance win. If you have a more even mix the answer might be different.

well, Rust's function call ABI is unstable, so Rust could add error types that propagate by unwinding (they would have to be guaranteed to unwind and not abort, so not quite like panics) even though the surface syntax uses Result<T, E> and ?.

4 Likes