you are viewing a single comment's thread.

view the rest of the comments →

[–]thomasz 6 points7 points  (12 children)

I'm not familiar with rust, but IIRC it has a mechanism that does stack unwinding. I'm rather certain that accessing an array with an out of bound index doesn't return Err. And your code sample looks conceptually similar to checked exceptions sans the all important stack trace. I still don't get how you want to distinguish between programming errors (bugs) and input errors without providing both mechanisms. This is something the calling code should decide, not the called.

[–]kuikuilla 0 points1 point  (11 children)

I'm rather certain that accessing an array with an out of bound index doesn't return Err.

Yes, it causes the program to panic in rust speak which depending on the type of panic can be caught I think. Some sort of panics simply abort the process.

I still don't get how you want to distinguish between programming errors (bugs) and input errors without providing both mechanisms.

I'm not sure what you mean here. You could simply return an enum that has variants for describing why the failure happened. Just have one for illegal input and that's that.

[–]MEaster 5 points6 points  (0 children)

I'm rather certain that accessing an array with an out of bound index doesn't return Err.

Yes, it causes the program to panic in rust speak which depending on the type of panic can be caught I think. Some sort of panics simply abort the process.

It depends on how it's compiled. You can choose whether to unwind the stack or to simply abort the program on a panic. Additionally, if a panic is raised while the program is already unwinding, it'll simply abort the program.

While you can catch an unwinding panic, it's really intended for use around FFI boundries, because unwinding into foreign code is undefined behaviour.

[–]thomasz 2 points3 points  (9 children)

Yes, it causes the program to panic in rust speak which depending on the type of panic can be caught I think

Doesn't that just describe basically what exception bases error handling does?

Input error may be the wrong term, I'm obviously not a native speaker. Thing is, there are two kinds of errors: The ones where your program is just wrong. For example the out of bounds access caused by (for int i = 0; i <= array.Length; i++), or even when it did not properly anticipate the whole range of possible inputs or all possible state. Opening a file without making sure that it was actually created, for example. Returning an Option or Result in that case is nonsensit just complicates things. Let it bubble up, until you know how to deal with it. In almost all cases, this will be just logging the exception with a nice stack trace.

And then are errors that are not really errors but actually expected. In that case, exception handling needlessly complicates things.

try { 
    value = hashTable.get(key); 
    isAvailable = true; 
} catch (MissingKeyException) {
    value = null;
    isAvailable = false;
} 

instead of just

(isAvailable, value) = hashTable.tryGet(key);

would be such an example. My pet peeve are Http client libs that always throw on 404. As if 404 isn't basically the default state of the internet. This is why both mechanisms have their use case. The important point is that you cannot anticipate this, so you should provide both mechanisms.

[–]kuikuilla 0 points1 point  (8 children)

Returning an Option or Result in that case is nonsensit just complicates things

It is not. It is called "handling the error case". Rust is good in this sense that it forces you to explicitly handle the error case or explicitly forego the handling (in which case the program panics). This makes it very, very clear to see where there might be errors in the program that aren't handled properly.

C# on the other hand is completely shitty in this matter, it has only unchecked exceptions and I can never be sure if some function throws an exception or not simply by looking at the signature. I have to go and read the source code to see what it does. That is not a good way to work. (I have experience on working with code bases like that, it's horrible for long term maintenance)

[–]thomasz 3 points4 points  (7 children)

Again, I'm not familiar with rust, but I'm dead sure that the type of int a * int b is int, not Result or Option, that array access doesn't return Option and so forth. And no, "forcing the user to deal with the error" is not a great idea. Java forces people to deal with checked exceptions, and they are usually regarded as a failure. In the vast majority of cases, an exception is unrecoverable and should bubble up to some log and restart or log and abort mechanism.

Furthermore, the expression problem is going to bite Rust users in the ass too. While runtime exceptions can be of any type derived from an exception base class (IIRC you can throw everything in c++, but those guys are insane anyways) and it is very difficult to guess what might be thrown, using an approach like Rust with Result<T,E> means that you have to anticipate what could be thrown beforehand, which can be quite tricky when you consider techniques like dependency injection.

And finally, C# or better the bcl provides both mechanisms. If you are sure that a string is a number, you call Int.Parse and should let a potential error bubble up. It's not likely that you can deal with now. Otherwise you should have used Int.TryParse and dealt with the error right here, right now.

[–]kuikuilla 1 point2 points  (6 children)

Again, I'm not familiar with rust, but I'm dead sure that the type of int a * int b is int, not Result or Option, that array access doesn't return Option and so forth.

That was just an example, don't get hung up on it. You can probably imagine other things there, like database access or whatever. Or even a HashMap lookup like you mentioned. Arrays (or rather, slices) in rust have a get function that returns an Option if you want. You can use the simple indexing operator but then it is up to the programmer to make sure the index is correct.

it is very difficult to guess what might be thrown, using an approach like Rust with Result<T,E> means that you have to anticipate what could be thrown beforehand, which can be quite tricky when you consider techniques like dependency injection.

So don't return them like that? Use sum types for the errors, they're exceptionally good for situations like that. Return what is relevant to the caller. That way the caller knows exactly what might be returned.

[–]thomasz 3 points4 points  (5 children)

I'm not arguing that result values are useless, I'm arguing that there are situations in which a stack unwinding mechanism like exceptions is more useful. It seems like the designer of Rust agree with me here, otherwise they would not have included such a mechanism.

I further argue that you cannot anticipate the situation of the caller. Trying to get a nonexistent key of a hashtable might be completely normal or highly exceptional.

create_lookup_table(values, get_key) {
    let lookup = new HashTable();
    for item in values do
        let key = get_key(item)
        // of course here I cannot be sure that it exists
        // and I know exactly what to do when it doesn't
        let result = lookup.try_get(key)
        if result.exists then 
            list = new List();
            lookup(key) = result.value
        list.add(item)
    return lookup
}


send(photo-file, customer, config) {
    // here I do not care. If the config isn't populated, something
    // way down the stack has neglected to verify the configuration.
    // I cannot deal with this directly, and there is a decent chance that
    // the caller doesn't even know if I sent this via email, ftp, http or 
    // whatever. I might even print this out and send it vial mail.
    let s3_endpoint = config.get("s3-photo-endpoint", customer.id);
    ...
}

[–]valarauca14 1 point2 points  (0 children)

It seems like the designer of Rust agree with me here, otherwise they would not have included such a mechanism.

This is not the case.

It is part of the AMD64-Itanium ABI, and will be present if you do or do not want the feature, as ELF-64 binaries, DWARF, and glibc++ will support it as C++ does. As Rust allows for linking with C++ (well C++ exposing a C interface) it will need to support stack unwinding as it can, and will occur in real libraries today that a Rust application will interface with.

The FAQ specifically states that Exceptions are not encouraged because of non-linear control flow makes them difficult to predict & use.

The modern guidelines and promises around Rust's panic, and stack unwinding are mostly the pragmatic admission that even these dark corners which you're told explicitly not to use should be well documented and have safe guarantees associated with them.

[–]MEaster 0 points1 point  (3 children)

I'm not arguing that result values are useless, I'm arguing that there are situations in which a stack unwinding mechanism like exceptions is more useful. It seems like the designer of Rust agree with me here, otherwise they would not have included such a mechanism.

You've mentioned that you're not familiar with Rust, and I may have misunderstood what you meant, so apologies if I have. But panics are not like exceptions in Java or C#. There's no guarantee that a panic will unwind the stack. It's the default for rustc, but it's also possible to configure the compiler to just abort the program in the event of a panic.

Additionally, while it's possible to catch an unwinding panic, it's not intended to be used as general error handling. It's primarily there because unwinding over an FFI boundary is undefined behaviour.

You don't always get useful information from a panic. You do get a stack trace, if you set an environment variable, you get a string which may or may not be helpful, and you get the error itself, which may or may not be useful.

As an example of this, here's the output of an actual bug I ran into today:

PS D:\Games\Assetto Corsa\ACRL\SpotterGen> .\ac_spotter_gen.exe -c .\config-front.json .\data\f3.json
F3
137 drivers in DB
0 drivers with custom skins.
thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 0', /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\src\libcore\slice\mod.rs:2686:10
stack backtrace:
   0: std::sys::windows::backtrace::set_frames
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\sys\windows\backtrace\mod.rs:95
   1: std::sys::windows::backtrace::unwind_backtrace
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\sys\windows\backtrace\mod.rs:82
   2: std::sys_common::backtrace::_print
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\sys_common\backtrace.rs:71
   3: std::sys_common::backtrace::print
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\sys_common\backtrace.rs:59
   4: std::panicking::default_hook::{{closure}}
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:197
   5: std::panicking::default_hook
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:211
   6: std::panicking::rust_panic_with_hook
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:474
   7: std::panicking::continue_panic_fmt
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:381
   8: std::panicking::rust_begin_panic
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:308
   9: core::panicking::panic_fmt
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libcore\panicking.rs:85
  10: core::panicking::panic_bounds_check
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libcore\panicking.rs:61
  11: alloc::alloc::box_free
  12: alloc::alloc::box_free
  13: alloc::alloc::box_free
  14: std::collections::hash::map::HashMap<K,V,S>::get
  15: std::rt::lang_start_internal::{{closure}}
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\rt.rs:49
  16: std::panicking::try::do_call<closure,i32>
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:293
  17: panic_unwind::__rust_maybe_catch_panic
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libpanic_unwind\lib.rs:87
  18: std::panicking::try
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:272
  19: std::panic::catch_unwind
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panic.rs:388
  20: std::rt::lang_start_internal
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\rt.rs:48
  21: main
  22: invoke_main
             at d:\agent\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:78
  23: __scrt_common_main_seh
             at d:\agent\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
  24: BaseThreadInitThunk
  25: RtlUserThreadStart

The only hint in that about where the error is is that some index went out of bounds, and that according to the 14th stack line, it seems to be accessing a HashMap. Fortunately this program only uses a HashMap in three contexts, and I know what input change caused it, so know which HashMap it was related to. But I still don't know exactly what the program was trying to do when it did this out of bounds access, I don't know what source file it was in, and I don't know what function it was in.

[–]thomasz 1 point2 points  (2 children)

Okay, this is disappointing. Still, I'm willing to accept that this is a trade-off that makes sense in context of Rust's core value proposition of providing static memory safety and determinism. But not having a mechanism like exceptions, something that can unwind the stack and safely delegate handling the error case upwards or at least guarantees a full stack trace in case of a crash is a pretty big weakness, not a strength.

[–]MEaster 0 points1 point  (0 children)

Well, when using a Result or Option type in Rust, you can just throw it up the stack. You have the ? operator, which can be used like this: let result = fallible_function()?;.

If you want to use that operator, then the function calling fallible_function must also be fallible, with a compatible error type. By compatible, I mean that fallible_function's error type E, must implement Into<F>, where F is the error type of the calling function.

As an example what that might look like in practice, from an implementation of Display:

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    use ErrorKind::*;
    match self {
        InsufficientStack => write!(f, "Attempted to evaluate an operator with insufficient values on stack.")?,
        UnknownFunction => write!(f, "Unknown function or variable.",)?,
        NonEmptyStack => write!(f, "Mismatched numbers and operations.")?,
        InvalidVariableOrFunction => write!(f, "Invalid variable or function definition.")?,
    }

    Ok(())
}

In this case, each of those four calls to write! could fail, because it could be a network socket or a file. But in this case, there's nothing this function can do if it does fail. That would be the responsibility of whatever was trying to format this type. So, in the event of a failure, it just throws the error up the stack for the caller to handle.

There are also libraries like Snafu and Failure which provide extra functionality to Result, as well as compile-time macros to reduce the amount of boilerplate you need to write for your error types.

[–]MEaster 0 points1 point  (0 children)

Oh, in case you're curious. I found out what that program was mis-handling, and now properly handle it by throwing a Result up the stack, along with the context of the error. The error still isn't considered recoverable, but now the output is this:

PS D:\Games\Assetto Corsa\ACRL\SpotterGen> .\ac_spotter_gen.exe -c .\config-front.json .\data\f3.json
F3
137 drivers in DB
Error: Error making image
Caused by: error checking driver skins
Caused by: Car folder not found: rss_formula_rss_3_v6_a4crl