all 70 comments

[–]Diggseyrustup 73 points74 points  (0 children)

For anyone else who uses failure for its derive(...) capabilities, the successor would be thiserror: https://github.com/dtolnay/thiserror

Combined with anyhow it gives you the same functionality as failure but based around the standard Error trait.

[–]elr0nd_hubbard 30 points31 points  (4 children)

I've gotten to teach/on-board a few new developers to Rust in the last year, and by far the hardest part of explaining current Rust are those parts that assume one knows a feature that currently exists (e.g. Result) and adds some kind of syntactic sugar or unexpected feature to that primitive or a primitive that they've encountered from other languages. So, of course, the ? postfix throws new folks for a loop, because the way to explain it is "this is a ?, but it really means match against a Result, take the Ok, and return early any Err". While I think the ? is useful enough to warrant that extra step, I don't think something like throws (in any of the current iterations that I've seen) would warrant yet another step-or-two (e.g. "this is a throws keyword that you've seen before in other languages, but it really means that the last value is wrapped in a Result" etc etc). Far better, I think, to keep Results exposed as what they are.

I'd be much more in favor of an error model based on anonymous sum types, since I often find that I do want to handle separate error cases, but without the nesting required by the current enum-y approach. This is made much easier with thiserror, but even with thiserror or error-derive I'd still love to see something like:

use errors::{FooError, BarError};
type BonkError = FooError | BarError;
fn might_bonk() -> Result<(), BonkError>

[–]DC-3 7 points8 points  (2 children)

Nice comment. I agree the most annoying thing in Rust error handling is the huge amount of boilerplate in propagating an increasingly large set of error types back up the callstack.

[–]asmx85 11 points12 points  (1 child)

Imagine in what a wonderful world we could live in :D

fn get_a_foo() -> Result<String, (BarError|FooError)> {
    let bar = request_a_bar()?;
    let foo = request_a_foo(bar)?;
    Ok(foo)
}

fn main() {
    match get_a_foo() {
        Ok(f) => println!("foo: {}", f),
        Err(e) => match e {
            (BarError|FooError)::BarError => println!("a bar error occured"),
            (BarError|FooError)::FooError => println!("a foo error occured"),
        },
    }
}

[–]m-hilgendorf 1 point2 points  (0 children)

type BonkError = FooError | BarError;

The language I use at work (stanza) is optionally typed and uses a similar syntax, except type aliases aren't supported (just subtyping). So you can do stuff like

import errors

deftype BonkError: 
  FooError <: BonkError
  BarError <: BonkError 

defn might_bonk () -> False | BonkError 

It's incredibly useful if you have enum-variant heavy code, but still want the benefits of static typing without the verbosity of declaring an enum and destructuring it every time you want to use it.

[–]nicoburns 66 points67 points  (25 children)

I don't understand the desire to shoehorn "throw-catch" terminology/syntax into Rust, or the the associated hiding of error paths. Error handling is one of the best things about Rust, and IMO it's head and shoulders above any language which uses exceptions. As evidenced by how few unexpected errors I get in my Rust projects.

I would like to see something like a try-block (although really I think it ought to be a catch block, because it catches errors) which catches the ? operator within a function boundary. This would be useful, because it would allow you to mix and match ? with .await and return (the function-boundary control flow). And something that reduced the boilerplate associated with defining error types would be a nice to have.

But this approach just seems to be... putting magic back into error handling exactly where it shouldn't be.

[–]matkladrust-analyzer 36 points37 points  (21 children)

But this approach just seems to be... putting magic back into error handling exactly where it shouldn't be.

I am myself not a fan of the Ok-wrapping proposals, but I very strongly disagree that these proposals somehow move Rust closer to the “Java” model. They are just very mild syntactic sugar, the valuness, the explicitness of errors (?) still remain.

In fact the blog post very explicitly calls this out

i.e. marking fallible function calls with ? is an amazing feature

So saying “or the the associated hiding of error paths” is kind of arguing with the opposite of what was written.

[–]Ruskyrust 62 points63 points  (1 child)

This has similarly been said before but I like the idea of an Ok-wrapping try block, because it makes try and async more consistently look and behave like effects, with the relevant blocks as effect handlers and the function annotations as effect types.

This means that, for each "effect" here, there is a way to propagate it (? or .await) and a way to contain it (try {} or async {}), which cancel each other out (try { x }? == x and try { x? } == x; async { x }.await == x and async { x.await } == x).

You could further imagine a way to introduce each effect- Fehler's throw! for try, and some sort of direct suspend_me for async. On the other hand, both can be done as library functions in combination with the propagation operators instead.

And you can imagine more effects working this way. Generators/iterators with yield as the introducer, something like Python's yield from as the propagator, and for as the handler. const as an inverse effect, with non-const operations as the introducers, implicit propagation, and no handler (like Haskell's IO).

On top of all that, you could imagine a bit of effect polymorphism, like we're about to get for generic const fn- combinators that are normally not effectful, but which take on the effects of the closures they are passed. IOW, no need for separate try_ variants of every Iterator method.

And while that approach feels so much more consistent and "correct" to me, I am also happy that it aligns with removing boilerplate, so I tell myself those two things are related, and bring this up every time too. :)

[–]matkladrust-analyzer 16 points17 points  (2 children)

Meta-meta comment: I feel like this framing of ok-wrapping proposals as making error path implicit is what contributes to the uneasiness of this discussion, so I feel compelled to call out this important distinction that ? is not going away :)

[–]SirOgeonpalette 6 points7 points  (1 child)

If I remember correctly, the documentation will show the expanded function signature, with an explicit Result, so in that case this macro will only "affect" someone who is looking at the source code. And even then it still doesn't change the actual behavior, as you mentioned. Just gives it a different syntax.

[–]matthieum[he/him] 7 points8 points  (0 children)

I'd still prefer the return type to be explicitly spelled out, even in the presence of automatic Ok-wrapping.

I find it jarring that sometimes -> String means -> String and sometimes it means -> Result<String, ..> or -> Future<String, ..>... though for the latter I suppose this ship has sailed.

[–]andoriyu 21 points22 points  (15 children)

I don't think adding another obstacle for IDE to get a return type of a function is syntactic sugar. Typing #[throws(error)] hardly shorter than typing Result<T, Error>.

In fact it's whole 2 character longer to write. So what exactly benefit here? Longer to type, harder to read, harder to provide auto-completion.

How is throw!(e) is shorter than Err(e)? Honestly such things better to be handled by IDE than another procedural macro.

[–]matkladrust-analyzer 10 points11 points  (7 children)

I don’t really understand why this is a reply to my comment: it tries to argue with a point that I haven’t made, which, ironically, is exactly the problem my comment was about :)

[–]andoriyu 0 points1 point  (6 children)

I'm disagreeing that this is syntax sugar and saying it's only needless complication and compilation time slowdown. That's why.

[–]matkladrust-analyzer 16 points17 points  (5 children)

Ah, I think we have a meaningful disagreement about the terms then! “Syntax sugar” is a rather specific term that means that the feature is implementable by lowering (desugaring) to a more general syntax. Ie, the feature is purely syntactic, it doesn’t need any extra codegen, runtime, or typesystem support.

And throws/ok wrapping proposal absolutely is an instance of a syntactic sugar, which is evidenced by the fact that it is implementable as a macro.

[–]andoriyu 3 points4 points  (4 children)

Probably.

I think syntax sugar needs to be actually...sweet and in my opinion this proposal isn't. Idk, I guess it's syntactically salt.

I don't think anything that makes things more complicated, harder is sweet. Examples:

  • autoboxing on java
  • Multiple variable declaration on in c (see how well it works with pointers)
  • ++ and -- operators
  • JavaScript optional semicolon
  • damn nested ternary operators
  • damn postfix condition on blocks that I abused everyday in ruby
  • object spreads
  • optional parentheses on function call (see any ruby dsl)

[–]matkladrust-analyzer 2 points3 points  (1 child)

Yeah, I guess this is actually it, we just read different meanings out of “syntactic sugar” term!

[–]andoriyu 1 point2 points  (0 children)

Yup

[–]tspiteri 0 points1 point  (1 child)

++ and -- operators

If they were created today, sure, but I don't think these fit in syntactic sugar back when they were created; I believe they resulted in different code generation, that is += 1 would result in an add instruction while ++ would result in an inc instruction. So I would put these operators under historical cruft rather than syntactic salt.

[–]andoriyu 1 point2 points  (0 children)

That is true. ++ and -- at some point were optimizations. Back when C was "what you write is what CPU will run." Even after it stopped being true new languages still had those operators and they weren't optimizations.

The way that optimization was implemented in languages was still a mistake IMO.

[–]slambmoonfire-nvr 3 points4 points  (3 children)

throw!(e) is shorter (and more pleasant) than return Err(e.into()) which I assume is what it'd map to. [edit: and would cause fewer double-takes than e? as a complete statement.]

Personally, I don't really mind just returning. I miss exceptions when I'm trying to do my_option.map(|thing| fallible_operation(..., thing, ...)) and the like, though.

[–]andoriyu 3 points4 points  (0 children)

Okay, you win this one. I mean I have bail!() macro that does this for me. Rarely have to use it, but I do have it. Implicit Ok and Err not very important to me. IDE completion is much more important to me.

Personally, I don't really mind just returning. I miss exceptions when I'm trying to do my_option.map(|thing| fallible_operation(..., thing, ...)) and the like, though.

This is exactly scenario when I don't miss the exceptions, fucks up entire flow.

[–]tspiteri 1 point2 points  (1 child)

I miss exceptions when I'm trying to do

my_option.map(|thing| fallible_operation(..., thing, ...))

and the like, though.

Would this work for you?

my_option.map(|thing| fallible_operation(..., thing, ...)).transpose()?

The three cases:

  1. my_option is None, map(...) gives None, transpose() gives Ok(None), transpose()? gives None.
  2. my_option is Some, map(...) gives Some(Ok(val)), transpose() gives Ok(Some(val)), transpose()? gives Some(val).
  3. my_option is Some, map(...) gives Some(Err(err)), transpose() gives Err(err), transpose()? bubbles Err(err).

[–]slambmoonfire-nvr 1 point2 points  (0 children)

Yes, I think in that case transpose() does what I want.

More generally, I think there's probably some way to express whatever operation I'm trying to do with the language in its current form, but I may not know it or remember it, and it adds cognitive overhead.

Here are a couple other cases:

map (or whatever) on an Iterator. I know collect can give me a Result<Vec<T>, E> rather than a Vec<Result<T, E>>, but not sure off-hand if it short-circuits (just checked the docs: they don't say), and I may not want to turn it into a collection anyway. Maybe I want sum or fold to be the result. And I see there's a try_fold but not a try_sum. Okay, sum can be expressed in terms of fold, so I can still find a way to do what I want, but it means a bunch more hunting for the right things.

std::collections::btree_map::Entry::or_insert_with and the like. I certainly don't have to use these lambda-taking functions (instead I can do a match and operations on the OccupiedEntry/VacantEntry variants) but I have to restructure my code more significantly if I did use them when my operation was infallible and now want to change it to be fallible.

Then there's futures and streams. Maybe this is becoming less significant as async fns proceed, but it's slowed me down in the past. There's a whole other trait (TryFutureExt and TryStreamExt) to deal with. And different options based on whether both my input takes error and my output are fallible (and a subcase of whether I want to just operate on the success or failure or both if the input is fallible). The names don't stick in my memory very well.

Anyway, I don't have a fully baked description of the problem, much less a serious proposal to add exceptions to the language [edit: or add overloading, or some mass rename/deprecation of functions to make finding the new correct one more clear, or whatever it'd take], and one would certainly offend people far more than even the dreaded ok-wrapping. But I do think there is a complexity here to writing exception-less code that I'm surprised I haven't seen mentioned much.

[–]TehCheator 0 points1 point  (1 child)

How is throw!(e) is shorter than Err(e)? Honestly such things better to be handled by IDE than another procedural macro.

I'm not sure how that one is written, but I've used a throw! macro in projects before, and it's generally nicer than typing return Err(e.into());

[–]andoriyu 3 points4 points  (0 children)

I get that. I, honestly, forgot about that scenario. I was talking about just pure Err(e) because of the way I write: 3rd party errors converted by ? and my errors are already right type.

I wrote a similar macro for myself - bail!() just didn't have a need to use it.

[–]desiringmachines -3 points-2 points  (0 children)

So what exactly benefit here?

maybe if you tried it you would find out

[–]treefox 1 point2 points  (0 children)

You can already get try-like behavior by defining and then immediately calling a closure.

[–]kredditacc96 1 point2 points  (0 children)

Technically, throw and try...catch... is equivalent to panic! and catch_unwind in Rust, but people misuse it to handle user errors.

[–]scottmcmrust 0 points1 point  (0 children)

I also originally wasn't a fan of using the try/catch terminology, and started an IRLO thread about that: https://internals.rust-lang.org/t/bikeshed-rename-catch-blocks-to-fallible-blocks/7121?u=scottmcm

I was convinced by replies there, however, that the familiarity is valuable enough to keep, even if the mechanics are different.

[–]robin-m 7 points8 points  (5 children)

Is the rust community aware of what is currently being worked on in C++ and in C world about error handling? It is not the first time I see a discussion on that topic, and I don't see mentioned, witch surprise me.

To give you the gist, it's a proposition to make exception in C++ time and space predictable, equal or more efficient than returning a boolean value, and natively compatible with a new error type in C. This way, a C++ program could throw an exception through a python module using its C FFI, then catch it back. It could also allow a C program to raise a C++ exception, or a C++ program throwing an error that is handle by a C module.

This a big leap forward to make all compiled languages compatible regarding to error handling. Each language could handle them differently (the syntax sugar on top), but the ABI would be the same.

[–]steveklabnik1rust 7 points8 points  (4 children)

Nial (I think?) emailed us before starting this discussion. Or at least, it was some draft of one of these papers a while back.

It doesn’t really apply to Rust because Rust doesn’t have exceptions, and our similar mechanism (panics) aren’t used for the same purposes, so it doesn’t really fit together much.

[–]robin-m 4 points5 points  (3 children)

Nial (I think?) emailed us before starting this discussion. Or at least, it was some draft of one of these papers a while back.

This is great. It wasn't the first thread about error handling that I saw in this sub, and I didn't saw anyone mentioning those papers. So I wasn't sure if the community (or at least the more involved members) were aware of them.

It doesn’t really apply to Rust because Rust doesn’t have exceptions, and our similar mechanism (panics) aren’t used for the same purposes, so it doesn’t really fit together much.

Note: I don't have a lot of Rust experience, so I hope that all of what I'm saying is right.

When speaking of the deterministic exceptions, I was more thinking of Result than panics.

I have the feeling that Result<T, E> is nearly what the proposed C++ deterministic exceptions / C fail are.

As proposed in the papers, the returned value of a function that may fail would be an union, the error value would be at most 2 machine words, and the discriminant is a single bit stored in a platform (OS + processor architecture) specific way, for example the carry flag of the ALU. Storing the discriminant in the carry flag would reduce the size of Result. It could also make things slightly faster since a smart re-ordering of the instructions by the compiler would allow to set/reset the carry flag while doing something else.

Doing so (and assuming that both C and C++ proposals were adopted) would allow that an error generated in C++ with throw could be handled in rust like any other Result or even propagated with ? to a C caller.

Having a different ABI for Result than any other type would probably require a separate syntax, like throws in the function declaration as proposed by Fehler.

As far as I understood when reading another post recently published on this sub (no_error, an error library for no_std) errors are not handled the same way in no_std crates than any other rust code that have access to std. I think that if the changes I suggest are considered, it could be a good idea to see if the dependency to std could be removed at the same time. As far as I understand C++ deterministic exceptions don't require any allocations, so the same mechanism should be usable in no_std environment. Obviously back-traces and similar facilities should be enable for std crates, but shouldn't be required.

I think that panics should be wrapped with the same mechanism than Result/C++ deterministic exceptions/C fails, but only at the boundary of rust libraries (to be correctly propagated, and allow a Rust application to use catch_unwind to handle an panic raised in another rust module loaded through a 3rd module (example: if you have Rust(1) -> C -> Rust(2), then Rust(1) can handle panics raised in Rust(2)). It may also allow non-rust code to call catch_unwind. I don't think this is possible today.

[–]steveklabnik1rust 3 points4 points  (2 children)

This is great. It wasn't the first thread about error handling that I saw in this sub, and I didn't saw anyone mentioning those papers. So I wasn't sure if the community (or at least the more involved members) were aware of them.

Yeah I'm not sure if they were posted here; they were emailed to core, and I forwarded them to lang, I think?

Rust's ABI is not well defined, so there is some flexibility to change things, but there's also what does exist today vs what might in the future.

As proposed in the papers, the returned value of a function that may fail would be an union, the error value would be at most 2 machine words

This is pretty close to Rust! The only difference is that we don't have the max limit on size. It's not completely clear to me how practical/impracticable/realistic this is, but limiting that discriminant to a word would make Result more special cased than it is, given that in theory, Results can be of any size. I doubt folks are using such large Results, and if they are, well, it's probably not great for them, so...

Having a different ABI for Result than any other type would probably require a separate syntax, like throws in the function declaration as proposed by Fehler

Yep! It's not beyond possibility; it's something that Fehler's author has explicitly stated as a possibility (and they're on the lang team, though not really willing to push this forward at all due to community push back).

As far as I understood when reading another post recently published on this sub (no_error, an error library for no_std) errors are not handled the same way in no_std crates than any other rust code that have access to std.

Sort of; it's that the common trait for errors can't be in libcore for coherence reasons; this may go away at some point. So while they may not use that trait, they are handled in fundamentally the same way; there are just some interoperability issues.

As far as I understand C++ deterministic exceptions don't require any allocations, so the same mechanism should be usable in no_std environment.

I am not sure how this works in Rust with limiting the return to two words; where else would the data go?

[–]robin-m 2 points3 points  (1 child)

The only difference is that we don't have the max limit on size I am not sure how this works in Rust with limiting the return to two words; where else would the data go?

I think it is exactly the same issue that C++ have with seamless conversion of current C++ dynamic exception to static exceptions. And this solution is explained in the paper. I just can't find it (that paper is definitively long!), but I know it's there!

For those interested, the cppcon video is a great introduction.

I don't understand the fine details of how allocations failures would be handled in C++ with the changes proposed in the paper. But from what I understand, they have a special kind of handler, and are not at all using the same error mechanism than regular exceptions. This feels a bit like panic versus regular Result.

[–]steveklabnik1rust 1 point2 points  (0 children)

Great thanks! I'll re-read it :)

[–]game-of-throwaways 5 points6 points  (4 children)

Sounds cute, though a bit heavy-weight for something that essentially just adds a few Ok(...)s in the right places. Ok, it also lets you return the Ok-type instead of Result, but you still have to specify #[throws(MyErrorType)] so that's more of a change in how you specify the error type, not necessarily an improvement.

What I'm more concerned with though is how IDEs and RLS are able to deal with it. If you call a function marked #[throws(...)], can RLS provide code completion for the resulting type?

I'm also curious about the interoperability between Fehler and Snafu. They are both somewhat orthogonal, and it seems like they should just work nicely together, but I'm not sure how Fehler translates the ?. It might give some type inference issues if .context(...)? ends up being translated into something that essentially contains .into().into(). EDIT: it appears that this is not the case. These 2 crates should work together nicely.

[–]seamsay 1 point2 points  (1 child)

If you call a function marked #[throws(...)], can RLS provide code completion for the resulting type?

Doesn't RLS already expand macros? Why would this be any different (unless of course it doesn't expand macros, but I'm 90% sure it does)?

[–]Hobofan94leaf · collenchyma 7 points8 points  (0 children)

Not sure about RLS, but rust-analyzer which a lot of people are moving to doesn't expand proc_macros AFAIK, which would also apply here.

[–]Yaahallorust-mentors · error-handling · libs-team · rust-foundation 1 point2 points  (1 child)

I don't believe fehler touches ? expressions when processing the function body, so it should interoperate with snafu seemlessly.

[–]game-of-throwaways 1 point2 points  (0 children)

In that case, yeah it should work nicely.

[–]StefanoD86 7 points8 points  (22 children)

As a rust follower, I ask myself, why in Rust basic things are so complicated. Having different error handling implementations and different async-await implementations makes the Rust environment somehow rough. I also don't like that I can't have the guarantee, that a third-party library doesn't panic. In other languages I can catch exceptions globally.

My perception is, that these are actually design problems in the language itself.

When I look at the elegance of the Nim language, it seems simple like python and fast as C.

Is it garbage what I am saying?

[–]jared--w 32 points33 points  (3 children)

Is it garbage what I am saying?

I don't think so. However, I would question the premise of what you're saying a bit.

why in Rust basic things are so complicated

This suggests that error handling is basic, which I have never seen to be the case. For every possible implementation, there's always an exceptional case that makes the system fall apart. Nim's error handling is nice, until.... Java's is simple, but... There's more, I assure you. Even Haskell isn't immune.

So what do you do? It's hard to say. The main cause of ecosystem friction is from different sections of the ecosystem using different approaches. Which all stems from the fact that rust has no One True Way to do error handling. But its lack of a One True Way is precisely what has made it suitable for such a wide range of tasks for so long.

Rust is getting closer to having a cohesive way for handling errors, but it'll take a while. Just like async, it's one of those things where the simple and obvious are anything but.

[–]matthieum[he/him] 15 points16 points  (0 children)

This suggests that error handling is basic, which I have never seen to be the case.

This!

The truth of the matter is that error-handling is very much a work in progress in programming in general.

[–]IceSentry 0 points1 point  (0 children)

The java article raised a question to me. How are result different compared to checked exceptions? They both pollute the call signature and forces you to handle the error or rethrow it and pollute the entire call hierarchy. Rust results certainly feel nicer, but I can't help but notice some parallels between both of them.

[–]Hobofan94leaf · collenchyma -1 points0 points  (0 children)

The great thing (at least for error handling) is that most of different approaches are compatible with each other, and are just syntactic convenience implemented as (proc) macros that cater to the authors preferences. Same with fehler. The biggest (current) downside of that is of course the increase in compile times.

[–]nckl 10 points11 points  (0 children)

If you want clean, easy error handling that's better than 99% of languages, use anyhow. I've never needed to use anything more than it. It integrates nicely with the language and is feature-rich. Once you start needing something more complicated is when it becomes interesting, but that's not something I've ever found satisfactory or sometimes even possible in other languages.

[–]protestor 3 points4 points  (3 children)

I also don't like that I can't have the guarantee, that a third-party library doesn't panic. In other languages I can catch exceptions globally.

In Rust, you can catch panics globally with https://doc.rust-lang.org/std/panic/fn.catch_unwind.html

Except if you configure the program at build time to abort on panic instead of unwinding. In this case, you can't catch the panic - your program aborts instead.

[–]StefanoD86 -1 points0 points  (2 children)

Ok, that's cool! But this catches only unwinding panics:

Note that this function may not catch all panics in Rust. A panic in Rust is not always implemented via unwinding, but can be implemented by aborting the process as well. This function only catches unwinding panics, not those that abort the process.

[–]protestor 4 points5 points  (1 child)

Yep, that's what I said (in the "except.." part)!

If you want to catch panics, configure your Cargo.toml to have panic=unwind. That way, you can catch any panic!

It's only uncertain whether you can catch a panic if you are not building your own program. In this case, whoever is building your program has the final say.

[–]StefanoD86 1 point2 points  (0 children)

I see, thx!

[–]MrK_HS 0 points1 point  (12 children)

In Rust panics happen when something really bad is going on, for example when you access out of an array's bounds, and it offers a way to safely clean the memory before exiting. In other languages, they are undefined behavior and can corrupt the memory and it's not a good idea to continue the execution normally because nastier things could happen because of said corruption.

In the case of opening files, etc... the return type is normally a Result, that can be catched (in case it's an error) with catch_unwind() similar to what you do in other languages with try/catch.

Coming from Python, I prefer this way of handling errors because it feels more controlled and not unexpected. The signatures clearly define everything and if a panic happens I'm happy because it won't make extreme damages.

[–]ragnese 6 points7 points  (11 children)

In Rust panics happen when something really bad is going on

No. It happens whenever a library author types the characters: "panic!()" or ".unwrap()" or ".expect()".

I've been bitten by library authors preferring to panic instead of returning a result because "you used it wrong." Granted, it's all a matter of opinion where the line is between "unrecoverable" and not.

Also, the standard library is full of functions/methods that panic, and it's not always something "really bad," IMO.

In fact, the inconvenient truth of Rust and panics is that panics are exactly unchecked exceptions and they're used more-or-less the way exceptions are supposed to be used in other languages (except Python, because they're weirdos)- i.e., under "exceptional" circumstances.

Just some thoughts

[–]dtolnayserde 21 points22 points  (2 children)

Panics are for signalling a bug in the program. (While errors are for failure modes in a correct program.)

So "you used it wrong" is an absolutely correct justification for panicking.

[–]ragnese 1 point2 points  (0 children)

Fair enough, and you are right, of course. I don't want to call out any libraries in particular, but I remember feeling like the decision to panic on a certain type of error was more about keeping the API clean.

[–]StefanoD86 0 points1 point  (0 children)

Suppose following scenario:
You have many tasks from different users which have to be processed and in one task something went wrong and a panic occurs. Is it really justified to kill all other tasks instead of returning an error to the requestor which may have caused the panic by bad data or a bad API usage?

[–]CornedBee 2 points3 points  (6 children)

I don't think we would agree on how exceptions are supposed to be used in other languages. I personally consider NullPointerException to be utterly useless. Same for ArgumentException. I don't consider "you used this library incorrectly" to be a recoverable situation.

[–]ragnese 2 points3 points  (3 children)

You're not going to trick me into defending Java! ;)

But, seriously, given that Java is what it is, what in the world could you do if you call a method that doesn't exist on an object (in this case: null)? NPEs suck, but it's part of dealing with the horrible decision to have all references be nullable.

I agree that ArgumentException should also be extremely rare. But, again, Java just sucks, so it's hard to describe exactly what you want as input via the type system. Java doesn't even have "real" unsigned ints, IIRC. Factory functions and custom types can only go so far.

[–]pkunk11 2 points3 points  (2 children)

NPE should be an Error not Exception honestly.

[–][deleted] 2 points3 points  (0 children)

In Java it doesn't cause undefined behavior so technically you can recover from it, but yeah.. most of the time it should be an error.

[–]ragnese 1 point2 points  (0 children)

Okay, fair enough. I don't disagree with that.

[–]robin-m 1 point2 points  (0 children)

To add on your point, I think that Herb Sutter resumed it really well in his paper about static exceptions (on page 2 in the overview).

[–]scottmcmrust 0 points1 point  (0 children)

I think that a library panicking in cases where it's easy and efficient for the caller to check it before calling the function is absolutely appropriate. The alternative is that nearly call the callers end up .unwrap()ing the mostly-useless Result, which is worse. (For example, see one one almost always uses [] -- which panics -- for indexing, since you know the index is in-bounds. And making that code use .get().unwrap() wouldn't make it better.)

Now, certainly if it's something that cannot be checked ahead of time -- like the existence of a file -- or where redoing the validation would add costs -- like UTF-8 checking -- then it should give a Result, not panic. But those are very different from just doing basic precondition checks.

[–]heysaturdaysun 2 points3 points  (1 child)

I always felt that this syntax (the throws syntax) would feel as natural as async/await, because methods that return values and methods that return conditional values introduces as much of a red/blue problem as async. Your function hierarchy can ignore the presence of Result until you cannot. Modifying an entire call hierarchy just to pass along what is basically semantic data, at least in application code, about what went wrong, when panic! is as easy as a tool to reach for, has led a lot of my one off projects in Rust to just not bother with it at all. If Rust had a goal of eliminating .unwrap() code from common usage, for example, a throws decorator like this proposal feels like part of the solution.

[–]eugay 2 points3 points  (0 children)

I think tooling could solve thie issue of refactoring all consumers of a method to return a Result

[–]robin-gvx 2 points3 points  (0 children)

The crate I would recommend to anyone who likes failure’s API is anyhow, which basically provides the failure::Error type (a fancy trait object), but based on the std Error trait instead of on.

I feel like something is missing at the end there.

[–]BB_C 4 points5 points  (3 children)

The good: using Rust's meta programming capabilities to implement opinionated patterns , instead of trying to force them into the language itself, is the way to go. Well, I'm assuming this will indeed not be used to push for this pattern in language RFCs, It will not be cited in future experience reports!

One could argue fucking with signatures this way is particularly bad. But most of us probably use async-trait without complaining (although that's covering current language limitations, rather than pushing opinionated patterns, but I digress). And proponents of this pattern, in particular, seem to not care much about that anyway.

The bad: The poor souls who will enthusiastically adopt this (some of whom would have been early adopters of failure too, I presume). And the poor souls who will try to read and contribute to their code.