Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Error Handling Is Hard (fpcomplete.com)
105 points by phonebucket on Nov 30, 2020 | hide | past | favorite | 111 comments


For this case, I actually think Python's approach is the best:

- Exceptions, so you don't need error handling

- stack traces so you know the entire call tree. This is pretty important -- without stack traces, it is very hard to make sense of error messages like "Cannot open file 'none'" -- the filename is clearly wrong, but where did this come from.

- chained exceptions so you can add context trivially. Doing "try: ... except: raise Exception('Cannot load config')" will print both error messages and two stack traces, so you know both original error and better description.

- verbose by default so if you don't put any effort into error handling, you get lots of details. This is somewhat controversial -- when some people see large stack traces their eyes glaze over and they just stop reading. But as a program's author, I love this -- the stack traces are informative enough I can often figure out what's wrong immediately.


I really, really dislike exceptions.

Unchecked exceptions don't tell the caller something might go wrong. Fine for Python, where strong guarantees aren't a thing anyway, but any statically typed language cannot be content with essentially adding bottom to every single type.

Checked exceptions have failed, or at least I haven't seen anybody fix their issues. They proliferate spurious exception types in interfaces. They are inflexible, as they usually can't be generic. They suck at typing error cases for higher order functions. They're big heavy and expensive, so can't be used for hot code paths. They're exceptions but more often than not you want to signal expected failure...

The list goes on...


Spurious exception specifications are the flip side of avoiding not telling the caller something might go wrong. It's a fundamental tension and is unavoidable.

Failure modes are an abstraction violation; they're a function of implementation. That's what makes checked exceptions not work, at the end of the day. Information-carrying exceptions reveal implementation details. So a module author must decide between hiding details and wrapping everything up in module-specific exceptions that user code can't actually use to make decisions most of the time, or expose implementation details that turn into a versioning problem over time.

There's roughly two situations for error handling: near the leaf of the call tree, where you have enough context to deal with an error, and need to switch on error type and take compensating action; or near the root of the call tree, in the main loop, where you log errors and terminate requests etc. in a generic way (e.g. 500 response).

Exceptions aren't ideal for the first situation but are great for the second. Error codes are adequate for the first situation - monadic error types (Result<T>, Either) are a bit better - but suck horribly for the second, because you need to manually unwind, writing boilerplate that should be automatic.

And at the limit, error types are isomorphic to checked exceptions, with the same problems, and more - error types introduce an aggregation problem, where multiple errors need to be joined together. You can still get that with exceptions too but it usually requires parallelism.


> There's roughly two situations for error handling: near the leaf of the call tree, where you have enough context to deal with an error, and need to switch on error type and take compensating action; or near the root of the call tree, in the main loop, where you log errors and terminate requests etc. in a generic way (e.g. 500 response).

There is actually a third case: in library code which calls other code which may fail. Take java.io.BufferedReader - to be usable, it has to be at a level of abstraction where it cannot deal with any errors the underlying Reader may throw; but the code using BufferedReader will have provided it with its underlying Reader, and will have a good idea of what errors are reasonable to expect from it.

The reason java's checked exceptions are so bad is that they cannot (or could not, before generics, and hence in most of the standard library do not) serve this use case, leading to checked exceptions that one really can't do anything with.


Sure, and there's also functional composition (functional code has the same problem - what does map(f) return if f throws?).

I think this is covered by the abstraction-breaking nature of failure modes, though. If your BufferedReader exposed the underlying Reader's failure modes, it's not just any BufferedReader any more, it's a BufferedReader<MySpecialReader>, and you don't get runtime polymorphism. You can write more generics to keep the polymorphism in static-land, but then you lose the ability to make choices based on error types.

The incompatibility is between errors and abstraction, not simply a single instance of composition.


> Exceptions aren't ideal for the first situation but are great for the second. Error codes are adequate for the first situation - monadic error types (Result<T>, Either) are a bit better - but suck horribly for the second, because you need to manually unwind, writing boilerplate that should be automatic.

At least in Rust, Result<t> can be unwrapped and bubbled up (in the error case) with a single `?`.


My hot take on this is that java checked exceptions are bad because their design predates java generics. Because of the lack of generics, authors of packages such as java.io had to create god-types of exceptions for their general interfaces to throw. A good example is

  public int java.io.Reader.read() throws IOException
As I see it, the purpose of checked exceptions was to allow declaring expected failure modes in the function signature, so that the programmer (and the compiler!) could check against them - but when my StringReader declares itself capable of throwing an SSLException (subclass of IOException), this benefit is lost. Instead, I must rely on other sources to determine which errors may actually occur, and which I can't do anything about - and the latter I must swallow or pollute all of my package's function signatures with. If the Reader interface had instead been generic

  java.io.Reader<T extends Throwable>
read() could be declared as

  public int read() throws T
This would rescue much, and is something that could be done in modern java; but by the time generics were introduced, all the core packages like java.io were written, and the patterns for how to deal with checked exceptions were set.


Yeah, I dislike also exceptions. I'm more fan of Rust error handling for example via `Result<T, E>`. I believe that make a distinction between recoverable and unrecoverable errors is key.


I see a lot of people espousing this viewpoint and I just don't get it. What's the big difference between the way Java does it and the way Rust does it?

In Java, unchecked exceptions are similar to Rust's panics. Sure your less inclined to catch panics than you might be to catching unchecked exceptions, but you can do it in either. Though perhaps Java's Error is closer to a rust panic.

Checked exceptions are just like the Result type IMO. The discoverability of the error surface is the same, you just don't get the nice pattern matching and sum types to handle/represent it.

What I'm getting at is that I'm a fan of the way Rust does error handling, but I don't get _why_ it feels different to the way Java does it. On the surface they're the same, but I loathe the Java approach. Maybe it's just whatever the opposite of rose-tinted glasses is.


As you say, the API feels similar. However, result types have many benefits over exceptions, with a few drawbacks.

With a result type, in most languages, you can't just ignore failure. You must handle it, if you want to access the value. This prevents the common Java bug where a checked exception is caught but not handled. In rust, you must handle the error, propagate the error, or panic and fail fatally.

Results don't abuse a runtime either. The exception process is slow - stack traces must be generated and the runtime itself needs to manually find the catch block responsible. Returning a result doesn't find this problem.

Checked exceptions are fine for exceptional cases. At risk of sounding facetious, that's why they're called exceptions.

But there's an important distinction that we need to make: logic failures are not exceptional.

If you expect an operation may produce an error, you should handle it or crash the program. You should do this without incurring a performance hit, and using reliable and transparent primitives. Result types achieve both of these criteria, checked exceptions neither.

The drawback to results is mainly the loss of data. Generating a stack trace is expensive, but often that's a worthwhile cost. Exceptions could be more versatile, in that any part of your code can throw any type of exception, but a result can only contain a specific type of error. That's not always a pro, though.

So in general, checked exceptions are pretty awful for expressing normal failure cases, such as not finding a record in a database. They aren't great for anticipated errors, such as not finding a file, because of the chance a programmer might allow this to silently fail. Runtime exceptions are highly valuable, however.

[1] - A decent SO post on the topic: https://stackoverflow.com/questions/613954/the-case-against-...


> This prevents the common Java bug where a checked exception is caught but not handled.

How does the Rust way avoid this? You can just as easily match on an error type with a catch-all case and ignore the error.


But you still acknowledge the presence of and explicitly supply a response to the failure case.


Is that not identical to an empty catch block? I fail to see how this is materially different


I believe the difference is that you resolve the error at the same point where you resolve a successful result, making it impossible for a reader of the function to, for example, assume that you can access a value after an exception was thrown.

  MyType x = null;

  try {
    x = doRiskyThing();
    x.doThing();
  } catch(CheckedException e) { ... }

  x.doThing();
This code makes it unclear where the exception is sourced from, and makes it seem like you can use `x` even in a failure scenario. In Rust, however:

  match doRiskyThing() {
    Ok(x) => x.doThing(),
    Err(e) => ...
  }
Here, it's clear where `x` is available and valid. You could even have the error path panic and merely return `x` if you wanted to use it later on in the method (borrow rules permitted, etc.)

A model that fits this more closely in Java is 'try-with-resources'.


I don't know that I agree that you're doing an apples to apples comparison though. There's nothing to stop you from doing:

  let x = doRiskyThing();
  match x {
    Ok(x) => x.doThing(),
    Err(e) => ...
  }
  x.get().doThing();
That would be more comparable to the try catch from above, and a comparable try catch that actually is semantically approximate would be:

  try {
    MyType x = doRiskyThing();
    x.doThing();
  } catch(CheckedException e) { ... }
Putting these two together, it makes the Result type seem worse than a try catch, right? But that isn't fair to result, because I'm using it like a jackass.

In other words, you can be a jackass using either of them, but you can also use them correctly, and when you use them correctly I don't see how they're any different. I also don't feel like the likelihood of using a Result correctly (edit: originally this said _incorrectly_) is substantially higher than a try/catch, but it probably boils down to developer experience and comfort with a particular varietal and not the capabilities inherent in either. Probably. And if not, I'm looking forward to being shown why! :)


I think I see what you're getting at, but let me point out why I still think it does something more than what Java exceptions allow:

  let x = doRiskyThing();
  match x {               // Pattern match on x
    Ok(y) => y.doThing(), // Giving the result a different name clarifies what's happening
    Err(e) => ...
  }
  x.unwrap().doThing();   // You can assume it's Ok, but you explicitly risk the application panicking
Of note, Result::unwrap is approximately the following:

  match x {
    Ok(y) => y
    Err(_) => panic!()
  }
x is still a valid value for the duration, and is never uninitialized; and whether you handle each case in a match or unwrap, both success and failure are considered and responded to before moving on. The Java checked exception code does that as well, except that it is unclear whether `doRiskyThing()` is throwing the exception, or `x.doThing()`; it's hard to reason about where the error originates from, and how much work was partially completed. In Java, the best way to circumvent this is - if possible - to keep try-clauses as tight as possible.


Your example smells like a failure of the type system. If that were TypeScript, for the simplest example, you'd explicitly have to type `x` as `MyType | null`, and were that the case, the `x.doThing();` outside the try block would not type-check.


It can be encoded in the type system as well. That's really what Java's checked exceptions are: one of the only places in Java where you can specify an alternative 'return' type. But it isn't quite complete enough to use in Java; little quirks here and there make it difficult to use widely, especially since Java 8 and lambdas arrived.

You can still use Java's type system to get something approximating Rust's approach. One upshot of Result-based error handling is that you can often replicate it in any language by shaving off one or two of the biggest benefits to suit that language's constraints.


> What I'm getting at is that I'm a fan of the way Rust does error handling, but I don't get _why_ it feels different to the way Java does it.

It's because of the way the exception information gets annotated. In Rust, exception information is part of the return type, so you can handle potentially-exceptional functions using mechanisms that are generic over any return type. For example, Iterator::map lets you call every function that might fail, and gather the results as failures, even if you don't fail-fast. But in Java, the exception information is passed in a separate part of the type signature, so generic functions have to be written differently to also handle functions that might fail.


Try to stick a function that may fail into a higher order function. The best example would be .map on streams/collections.

You can only do it with unchecked exceptions in Java. In Rust, you can transparently do it with iterators and the result type.

So given xs: [A] and f: A -> Result<A, E>, it is trivial to typesafely get Result<[A], E> by xs.map(f) where map: forall T. [T] -> (T -> S) -> [S]. This is outright impossible with Java. You have to circumvent the type system, or emulate Rust's approach.


Maybe I'm missing something but why does this not work with Java?

Is it the fact you would put your xs.map call in a mapper or something?

Because I know for sure you can do it in Kotlin: https://github.com/michaelbull/kotlin-result

But I don't see reified generics as a requirement for what you describe, and that's the main Kotlin-only feature I see being used


The idiomatic error handling mechanism of Java is the checked exception. This mechanism does not work with higher order functions.

You can emulate Rust's approach with Java, by creating what is essentially a sum type like Result. You'd have to enforce that any access to its content must also handle the error case, and I don't really know how to do that generally. There are various hacks, like having a bespoke sum type for that particular operation that twrows a particular exception on access to its content. But that gets really old, really fast.


Kotlin doesn't have checked exceptions, so being able to do it in Kotin is irrelevant.


What?

a) I asked how is it not possible in Java. The Kotlin library is just an example of something I use, it's not relying anything impossible in Java as far as I can tell... like I said reified generics don't change what the calls would look like, just what the implementation looks like

b) What do checked exceptions have using Result? The whole point is you use Result monads in all your code instead of exceptions. When interfacing with legacy code you wrap any exceptions in a Result class as well.


With checked exceptions it's not clear how control can pass into the catch block (using Java terminology). The fact that a method can throw a particular exception is not visible at the call site so unless you examine the declaration or definition of every method call within the try block, you cannot be sure from where control can jump to the catch block.

And if there are multiple places within the try block where the caught checked exception can be thrown, then basically the catch block can do nothing sensible with respect to recovery or even guaranteeing anything about the current state of the world once you're in the catch block.

I've seen some horribly ugly patterns used in an attempt to deal with this - basically you need to explicitly record progress within the try block which can be tested in the catch block.

And whatever about Java - at least the design decision hasn't completely poisoned the subsequent development of the language. Whenever I use C++, it feels like a massive amount of the complexity, wizardry and arcane/complex patterns required to write "safe" C++ would not be necessary if they hadn't added exceptions to the language early on.


You might enjoy this 2005 blog post from the great Raymond Chen titled Cleaner, more elegant, and harder to recognize.

https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...


I have to agree with the author. It is extraordinarily difficult to see the difference between bad exception-based code and not-bad exception-based code. In particular, the example of bad vs not-bad doesn't show substantive differences between the two versions.

    // bad
    NotifyIcon CreateNotifyIcon()
    {
      NotifyIcon icon = new NotifyIcon();
      icon.Text = "Blah blah blah";
      icon.Visible = true;
      icon.Icon = new Icon(GetType(), "cool.ico");
      return icon;
    }

    // not-bad
    NotifyIcon CreateNotifyIcon()
    {
      NotifyIcon icon = new NotifyIcon();
      icon.Text = "Blah blah blah";
      icon.Icon = new Icon(GetType(), "cool.ico");
      icon.Visible = true;
      return icon;
    }
What is the problem with the first version and how is the second version fixing it?


I agree the article might have been more explicit there.

If I'm understanding correctly, it's all about possibility of this statement throwing an exception, as for example the cool.ico file might not be found or might be corrupt:

   icon.Icon = new Icon(GetType(), "cool.ico");
I think the point is that the 'bad' version can make the icon visible in the UI and then throw an exception. The 'not bad' version has a more transactional flavour: if the aforementioned statement throws, then, because of the better ordering, the 'not bad' version doesn't make the unfinished icon object visible, it just bails out with the exception having made no change to the UI, and the unfinished NotifyIcon instance gets garbage-collected.

I presume that the real-world code included some extra machinery to hook it up to the existing UI objects, omitted for brevity in the example. It's a bit confusing as it looks rather like it's just building up and returning a NotifyIcon instance for the caller to make use of, but I think the icon.Visible = true; is meant to represent truly making the icon visible in the UI.


Thanks. Then the 'bad' code should look something like:

    // bad
    NotifyIcon CreateNotifyIcon()
    {
      NotifyIcon icon = new NotifyIcon();
      icon.Text = "Blah blah blah";
      system.Display(icon);
      icon.Icon = new Icon(GetType(), "cool.ico");
      return icon;
    }
Which would make noticing the coding error quite a bit easier, as we are passing an obviously incompletely initialized object to a third party. As presented, the code presumably is intended to be used like so:

    NotifyIcon icon = CreateNotifyIcon();
    system.Display(icon);
which works in both versions. If 'icon.Icon = new Icon(GetType(), "cool.ico");' throws, then the NotifyIcon is never displayed.

As presented, this is hardly an argument that explicit error passing code is better at detecting bad code than exception based code. His other article, https://devblogs.microsoft.com/oldnewthing/20040422-00/?p=39..., is making the point a bit more clearly.

The point itself is half-sound. While it is true that with exception based code, it is hard to distinguish which statements may fail, the real issue is about correctly releasing resources. Assume that every statement can fail, and use some form RAII to manage resource cleanup. The exact content of RAII is hard to glean in both explicit error code and exception based code, as it depends on how the specific third party API acquires/releases resources. Though APIs can be organized to essentially force the use of RAII, even if popular but ancient APIs like POSIX file system are not designed that way.

The real language design issue is that Java/C# mechanism for running cleanup code 'try {} finally {}' a. fails to pass through a handle to the resource that needs cleanup and b. is not scoping the lifetime of the resource that needs cleanup. Furthermore, the standard API makes no effort to provide RAII constructs for correctly managing resource lifetimes. Those language ecosystems actively steer people towards writing bad code. Exceptions may be fine, but the lack of language supported RAII is definitely poor language design. For better language design, exception based Python offers 'with' mechanism and error code based Golang offers 'defer'.

https://www.python.org/dev/peps/pep-0343

https://golang.org/ref/spec#Defer_statements


> Assume that every statement can fail, and use some form RAII to manage resource cleanup.

I think that's part of Chen's point: programmers just aren't good at doing this, they even get it wrong in published code samples. Exception-handling tends to become an afterthought, and even if you do pay attention to it, it's hard to get right.

Chen is hardly alone in his scepticism. Exceptions are forbidden in the Google C++ style guide. They're also forbidden in certain critical-systems subsets of languages. Ravenscar Ada forbids exceptions, [0] as does Spark Ada (though in that case it's for a slightly different reason: it's difficult to formally reason about exceptions). edit Apparently the JSF C++ standard forbids exceptions, but MISRA C++ permits them provided certain guidelines are followed.

I agree that RAII is very useful for robust exception-handling.

> APIs can be organized to essentially force the use of RAII, even if popular but ancient APIs like POSIX file system are not designed that way

Right, this is just the kind of thing C++ wrappers add (when wrapping C APIs).

> Java/C# mechanism for running cleanup code 'try {} finally {}' a. fails to pass through a handle to the resource that needs cleanup

Short of proper RAII (destructors), I'm not sure what that would look like.

I'm not sure what Chen makes of destructors. They're non-local flow-control, but he seems to like them.

Somewhat related: Zig's optional types, which essentially force the programmer to explicitly handle the case where the data doesn't exist. [1] Much more robust than the approach C takes, where the programmer is trusted to perform the check when necessary.

[0] p20 of PDF: https://www.sigada.org/ada_letters/jun2004/ravenscar_article...

[1] https://ziglang.org/documentation/master/#Optionals


RAII can be decoupled from ctor/dtor mechanism, see Python's 'with' statement. Javaish pseudocode, similar to a 'for' loop:

    // on exit the finally fn will be called with 'x' as an argument.
    with (Type x: create(args); finally (Type x) -> cleanup(x)) {
      // safely use x, throw at will.
    }
Re C++ and exceptions, there is an additional layer of wrinkles: throwing exceptions from ctors or, worse, dtors. Herb Sutter used to have a loooong list of what can go wrong in such situations back in the day. Explicit error codes happen to make it impossible to write ctors / dtors that can enter an error state by virtue of the fact that there is no way to return the error. My suspicion is that this is 50% of the reason of banning C++ exceptions in solid C++ style guides.


> RAII can be decoupled from ctor/dtor mechanism, see Python's 'with' statement

That may be a useful language feature, but I don't count that as full RAII. I'm inclined to agree with Wikipedia's definition that the destructor must run whenever the object's lifetime ends, to count as proper RAII:

> resource deallocation (release) is done during object destruction (specifically finalization), by the destructor

As x can presumably leak from the with block of your example, it isn't full RAII. I think it's equivalent to the confusingly named using feature of C#. [0]

> throwing exceptions from ctors or, worse, dtors. Herb Sutter used to have a loooong list of what can go wrong in such situations

Thanks for the pointer ( or should that be reference? :-P ), these are an interesting read. [1][2][3]

> Explicit error codes happen to make it impossible to write ctors / dtors that can enter an error state by virtue of the fact that there is no way to return the error

Yes, but this approach detracts from the RAII somewhat, as it means the programmer must move the substantial code out of the constructor and into a post-construction initialization member-function capable of throwing. That may be a price worth paying, but I do consider it a price, as we've lost a useful invariant: we must now keep track of which instances are in the constructed-but-unready state. An unfortunate slide back toward C.

> My suspicion is that this is 50% of the reason of banning C++ exceptions in solid C++ style guides.

I suspect these weird edge-cases where intuition diverges from reality, are much of the motivation for those prohibitions, yes.

[0] https://docs.microsoft.com/en-us/dotnet/csharp/language-refe...

[1] http://www.gotw.ca/gotw/047.htm

[2] http://www.gotw.ca/gotw/066.htm

[3] (Rather more basic) https://herbsutter.com/2008/07/25/constructor-exceptions-in-...


Walter Bright (D's author) said something like: who's going to check the error returned by the log functions?

I would add if you want to be correct then the addition signature should be (IntX, IntX) -> Either<IntX,Error>. Are you going to check also the return value of every arithmetic operation?

For me potentials errors are everywhere, so exception aren't bad, even if they make writing 'exception safe' code hard. That's why I was quite interested in the Vale language which claims to improve RAII: https://vale.dev/blog/next-steps-raii


What about this middle ground? These are my thoughts, very curious whether there are languages doing this & if so, what the real world problems are.

1. Statically typed.

2. Unchecked exceptions by default. IE, implicit bottom on every type.

3. Optional checked exceptions. Implementation could be `checked` keyword (e.g. `checked fun foo() throws XyzError {}`), or a generic type, or something else depending on the language. In order to mark something as checked, it must catch all exceptions from unchecked values it touches.

4. The compiler infers exceptions types when possible. So, if a regular unchecked function (`fun bar() {}`) only calls checked functions (foo from above), the compiler will carry the information about what can be thrown (XyzError) up the chain, even if there is no explicit signature. This way if another function (`checked fun baz() {}`) calls bar(), then it only has to catch or declare the XyzException, rather than all exceptions generically.

Finally, and to tie this all together: as a matter of style, reserve exceptions for actual unexpected errors. To use an example from downthread, a JSON parsing library should not be throwing parsing errors, it should be returning a Maybe<Parsed, Error>. Why? Because the library cannot determine whether the error is unexpected. If you're using it to parse user input, then yes, you're going to frequently encounter invalid data, and this should be part of your normal control flow, not something that can crash your application at runtime (you may want to log the event, maybe with a stack trace, but without interrupting normal operations). On the other hand, say you're parsing JSON from your database, (also, why are storing json in your db? But bear with me..). And invalid json means data corruption, which means your db is fubar. Then, it's fine to throw, because in that case you're operating on state that you assumed to be impossible, and your program behavior is now undefined.

The key point here is that exceptions are useful as a safety valve for early termination in the case of invalid assumptions. That's a subset of all error handling. If you try to force all error handling into the same paradigm and the same level of reliability, you're going to have awkward edge cases.


Reply to self: barrkel's comment upthread describes the premises I was working off of quite elegantly: https://news.ycombinator.com/item?id=25260631


I think your post misses the point somewhat.

One question is whether the thing happening is really unexpected to begin with. Why would you throw an exception when you know you're dealing with the file system and a file might not be there?

If you treat all "tainted" contexts are error-prone and implement proper error handling and relevant logging, you might never need to throw an exception because those situations aren't really exceptional to begin with.

Another you might ask yourself is whether your language of choice has stack traces that are easily parsable, because you might not be allowed to expose all app internals in all contexts.

A third is whether it pays to have that high a verbosity in your logs. In other words, do you really need a stack trace? Is it worth the performance overhead? Is it worth the code clutter of having try/catch/finally's everywhere?

A lot of this comes down to preference, but I think there's a valid case to be made for the approach that Exceptions are only for truly exceptional cases. Like nulls, god objects or deep inheritance hierarchies, they are vastly overused in my opinion.


For a language like Rust, exceptions wouldn’t be very nice. Actually, scratch that: for a type system like Rust’s (specifically, algebraic data types and static typing), exceptions aren’t very nice. Conversely, for a type system like Python’s, Rust’s result-based errors would be downright terrible.

I’ve steadily been concluding that, in a vacuum, result-based errors are vastly superior to exceptions for potentially-recoverable errors, but that you absolutely require static typing before the ergonomics become acceptable.

One important thing to also realise is that result-based errors aren’t actually incompatible with stack traces, chaining or being verbose by default, though Rust chose to eschew those features in its standard library, because it wasn’t clear (and still isn’t) exactly how it’s best to do that, especially if you’re trying to minimise cost, which a language like Rust is. That’s why there are so many experiments, such as anyhow.

Result-based errors is also excellent at some forms of composition that exceptions are completely useless for; for example, in Rust an iterator can yield results, and most file system stuff does that, so you can use things like map, filter, &c. on the iterator—you can even collect `impl Iterator<Item=Result<T, E>>` to `Result<Vec<T>, E>`, easy early return semantics. Anything like this is not at all elegant in Python.


I agree with this. I have tried briefly to make use of Result and Option types from https://github.com/cognitedata/Expression, and using match from pampy, in a personal project and while they are quite nice, it just wasn't fun at all compared to Rust.


I agree with you. It's funny that probably Java exceptions are famously hated on in the programming community while fulfilling those exact points as well. I'd much rather deal with too verbose exceptions (anyone seen them from Jenkins maybe?) than the minimalism that is the default in many languages nowadays.


The issue with Java exceptions is you get a lot of APIs that just "throw IOException" like this API to parse a JSON string [1]. Note that JsonParseException is a seperate exception and IOException mentions in the API docs that it's caused by "network errors or unexpected end of file", which is impossible here (missing close brackets are a JsonParseException).

It's because some nested implementation detail API several layers done can take arbitrary streams, and the String based implementation is done by creating a stream for that string, but the stream api allows for IOExceptions in the case of streams from disk or network devices etc.

What reasonable recovery option is there then? You can swallow the exception, because it's impossible for now, but your linter/teammates will complain at you and it could change in future to be possible since it's in the API contract. You could have your application re-raise the exception, propagating the problem further. Neither option is great.

[1]: https://fasterxml.github.io/jackson-databind/javadoc/2.7/com...


You are indeed right, and this is a nuisance. But there is a "correct" solution imho, which is to write:

    try { 
        ....
    }
    catch (IOException e) { throw new RuntimeException(e); }
It asserts "I believe this error can never happen - so if it does, just re-raise it and it'll get to the top level and the request or program will terminate - which isn't a problem, as, as I've said, this can never happen".

As we all know here, sometimes things that can never happen do happen e.g., as you stated, you initially wrote the program knowing it took a stream based on a string, but later that got changed to something that reads from a network. Having that error raised and logged top-level is the ideal outcome if such a mistake is made.

This syntax is verbose and I do wish there were a better syntax (e.g. Lombok @SneakyThrows is a hack which improves the situation somewhat) but the approach is the correct one.

And it also shows that you've thought about the exception. I even often find myself putting a little comment, mainly meant for future developers reading the code to whom it might not be obvious why this could never happen:

    // Can never happen: Stream is always a string
    catch (IOException e) { throw new RuntimeException(e); }


Almost. The right answer is to throw new JsonParseException(e) instead. That says that yes, this exception can happen, it's a JSON parse error. But the "outer" exception stack trace will lead to this catch block, which doesn't give all the information. But the exception wraps the "inner" exception, which has the stack trace to where the actual problem was detected. But the inner exception had the wrong type (IOException), so you wrap it with the outer exception to correctly convey what went wrong.


Throwing RuntimeException is never correct.

    throw new UncheckedIOException(ex);


> some nested implementation detail API several layers done can take arbitrary streams, and the String based implementation is done by creating a stream for that string, but the stream api allows for IOExceptions in the case of streams from disk or network devices etc.

> What reasonable recovery option is there then?

adrianmsmith's answer seems to have it, here are some rather less useful thoughts, for good measure:

The C++-style approach might be to have a boolean template parameter in the Stream API, allowCheckedExceptions, to change the signatures accordingly. As Java lacks specialisation, I don't imagine there's a way to do anything similar, short of having the library offer two very similar Steam APIs, one that permits throwing of checked exceptions and one that doesn't.

edit I suppose the standard library could offer a wrapper around the Stream API, to only accept non-throwing streams, and to return a NonThrowingStream. Internally this wrapper would presumably use the strategy adrianmsmith explained.


Well, what this API's implementer could have done, is to catch the IOException and throw RuntimeException. That makes sense, because there is no reason during normal program operation for this API to throw IOException, so if it does it must be an internal bug in the library.

This pattern is equivalent to Rust's `unwrap` and `expect`, than can be called to transform a recoverable error into a panic.


Wouldn't you have that same issue in Rust though?


Well, same issue and same solution (see other answers), except that in Rust, instead of `RuntimeException` it's named `panic`.


After learning how Rust/Haskell do error handling and how to use it, going back to Python's exception causes almost physical pain.

There is no distinction drawn between "small error" and "catastrophic failure", anywhere, at anytime, some piece of underlying code may throw nearly nearly any exception up at you by surprise which is a fun adventure in "what will fail next when I shift my attention away slightly". So few libraries properly utilise context: "ValueError on line 1:44" in whatever large file you're reading through (looking at you Pandas), doesn't tell me what the data was, nor what it was expecting, or the real line/position in the file or give you any way of recovering and resuming.


+1 on verbose by default. I've seen a lot of "error occurred" dialogs which were absolutely useless. Vs reading a verbose stack trace: trivial to back trace and say X passed this data from Y. Even for areas I wasn't an expert it. Made finding the next owner to investigate significantly easier.


I write a quite a bit of [no_std] Rust code (i.e. with no standard library) and you don't get things like a memory allocator or even a panic handler out of the box. However, I find that creating a custom error enum for each of my modules and piping errors into errors by implementing the From trait for each Error enum referenced inside those modules is not as tiresome as one would expect. It frees me up to use the ? operator wherever I want and add whatever "context" I want by using structured enums. That way, when an Err is returned instead of an Ok result variant you get a pseudo-stacktrace via your error chain and the error variant itself can have data in it (like an index or a key that could not be found). It's up to you how specific or generic you want it to be. You can also use "map_err" to decorate fallible code you don't control with extra contextual information that will help you locate the source of the error.

For example:

`Error: CookingError(CakeBaker(TemperatureCOutOfRange(999, 250)))`

This has the added benefit of not forcing your error handling methodology on users of your libraries.


Good article. I've always argued that blind ?-style shortcircuiting in Rust is extreme code smell and guaranteed pain for future production outages, and that it's not an actual solution to error the handling problem (which seems to come up often in Rust/Go flamewars). And I'm glad that this idea is picking up traction.

Error handling is hard, Go doesn't hide the complexity, but a lot of other languages try to handwave the complexity away. Either way, programmers are by default lazy, and you end up with "Error 500: connection refused".


Granted, even though Go shoves its errors in your face, it's still common to just punt the error up the stack (if err != nil { return err }), leading to ahem "context-free" error messages like "Failed to initialize Foo: EOF".

You can improve on this a little by using something like https://github.com/pkg/errors, which gives your errors a stack trace by default. But now your errors are hideous and unlikely to be fixable by the average user.

Ultimately the only way to get clean, informative, and debuggable error messages is for the programmer to attend to each and every one. And no one wants to do that, because it's a pain, it slows you down, it gets in the way of solving The Problem At Hand. So unless the language itself somehow forces you to wrap all your errors in nice context strings, it ain't gonna happen.


> Error handling is hard, Go doesn't hide the complexity, but a lot of other languages try to handwave the complexity away. Either way, programmers are by default lazy, and you end up with "Error 500: connection refused".

Except that (no pun) Go has both exceptions in the forms of panics AND its horrible error interface. I don't like exceptions but I'll take exceptions over Go glorified return codes anytime. No amount of "decorating" that system makes it better. There is nothing to it. Rust is a bit better on that matter BECAUSE Rust has the syntactic sugar to make errors as value painless to deal with. Go doesn't.

Sometimes on your server, you have 15 IO operations to perform, you just want to log on failure and return error 500 to the user. This is the case where go doesn't shine at all, what do you do? aside from being tempted to resort to a GOTO statement?


You check each one for an error and return a 500 if there's an error. It takes a few more seconds to write but is incredibly easy to read and maintain.

I'd argue that this article is evidence that Rust's syntactic sugar doesn't really help, it just seems like "official" encouragement to do the wrong thing (pass the buck).


> It takes a few more seconds to write but is incredibly easy to read and maintain.

I disagree.

    try{
        dothis();
        /* ... 14 lines of dothis(); like */
    catch(error){
        log(error);
        return responseCode(500);
    }
sounds easier to both write and maintain to me. Only a handfull of times you'd ever want to deal with each exception separately. And when you do, it's not much more verbose than if err!=nil{ ... }.


.. except in Java and Python, where you also get 100-line stack trace in your error log. Or C/C++, where you get a full coredump with program state as long as you set up your system correctly.

Let's not forget there are more options than Go's and Rust's approaches. If some languages made error handling hard, it does not mean it is hard in general.


One of my longest and most frustrating debugging sessions was with a Java project where some developer was fed up with 100-line stack traces and wrote the following:

    catch (Exception e) {
        log.error(e);
    }

Which caused the following error output

    ERROR: could not connect
(mentally adjust to Java syntax, error message, don't remember off the top off my head)

I'm not joking. Having overly-verbose errors by default will make people just swallow everything at some point or another.

That particular example was triggered, IIRC, by a configuration file containing a malformed IP address, which was interpreted as a hostname, which resolved into an NXDOMAIN, which caused a magic connect-socket-to-dns-name-or-ip-address call to fail with a generic message when that socket was lazy-connected in an unrelated code path.


Yep, people keep doing this. I think it depends on background.

If the app is to be used by user unfamiliar with language, and there are no robust error reporting systems -- think website or desktop app -- then this approach actually makes sense. But in the environment where most investigation is done via the logs, and many bugs are not trivially reproducible? Not so much.


Following through to the logical conclusion, you would want some way to switch between simple logs and context-rich logs. We've reinvented the verbose flag!


btw. there are error loggers which would print a stack trace in that case. i.e. slf4j. you can even do log.error(e, "blablabla");


As the author of [SNAFU], another error handling library, this is how I'd encourage it:

    use snafu::{Snafu, ResultExt};
    
    #[derive(Debug, Snafu)]
    enum Error {
        #[snafu(display("Failed to read {}", filename))]
        ReadInput { filename: String, source: std::io::Error },
    }
    type Result<T, E = Error> = std::result::Result<T, E>;
    
    fn main() -> Result<()> {
        let filename = "input.txt";
        let s = std::fs::read_to_string(filename).context(ReadInput { filename })?;
        println!("{}", s);
        Ok(())
    }
    
Unfortunately, this uses the default `Debug` formatting, so what is printed to the user in this `main` example is still less-than-ideal:

    Error: ReadInput { filename: "input.txt", source: Os { code: 2, kind: NotFound, message: "No such file or directory" } }
I encourage using the `Display` formatter or something more complete.

I find that doing this rigorously creates what I call a "semantic backtrace", where the linked list of (error, context, underlying cause) provides a great deal of insight into the problem.

The next release of SNAFU will incorporate something that allows for stringly-typed errors:

    use snafu::{ResultExt, Whatever};
    
    type Result<T, E = Whatever> = std::result::Result<T, E>;
    
    fn main() -> Result<()> {
        let filename = "input.txt";
        let s = std::fs::read_to_string(filename)
            .with_whatever_context(|_| format!("couldn't read {}", filename))?;
        println!("{}", s);
        Ok(())
    }
You will also be able to combine stringly-typed errors with the structured errors.

Other features of the currently-released SNAFU:

    - Custom error types
    - Backtraces
    - Extension traits for Results / Options / Futures / Streams
    - Suitable for libraries and applications
    - no-std compatibility
    - Generic types and lifetimes
[SNAFU]: https://crates.io/crates/snafu


How does this work if I use SNAFU and so does a library I call?


I'm not 100% sure what you mean, so I'll try to cover it broadly.

Ideally the fact that you use SNAFU should not escape from your library; I encourage creating an [opaque] error in most cases.

The current deliberate "leak" is that we implement a [trait] that exposes the ability to get a backtrace from a SNAFU error because the standard library has not yet stabilized backtraces. Once those are stabilized, we should be able to completely isolate and only operate through the `std::error::Error` trait.

[opaque]: https://docs.rs/snafu/0.6.9/snafu/guide/opaque/index.html

[trait]: https://docs.rs/snafu/0.6.9/snafu/trait.ErrorCompat.html


Context is quite important, I agree.

However, when possible, giving _actionable_ info can really help, especially if the user can see that error.

For example, you can try to explain why this file is expected, and give a link to the spec/doc. Extremely helpful when called at 4am in production.

Alternatively, I've seen some experiments with extreme reproducibility: capture the full context (network requests, filesystem, db state, etc.), and spin up an instance with that state. This way devs can deterministically reproduce the issue, and fix it. But, it supposes that the application is written to support this fairly well (by either being very functional, or by having probes in all non-pure functions).


You can't always have actionable context, because generally if you could, you wouldn't throw the error in the first place. :) I mean, apart from the obvious 'database connection failed, check that it's up'.

Troubleshooting hints should live in production runbooks, which tell you, for any firing alert, what is it that might cause it, how to investigate further, where the code lives, etc.


Surprised no one has mentioned the color-eyre[0] crate yet, which gives very nicely formatted errors with stacktraces if desired.

The author gave a great talk about error handling at this year’s rust conf[1].

[0]: https://docs.rs/color-eyre/0.5.4/color_eyre/ [1]: https://m.youtube.com/watch?v=rAF8mLI0naQ


In my job I am developing an app in C#. When I joined the team, there was already a Result library which is just a wrapper for return types with predefined set of result codes and a field for error message. It was first used for our bridge with JavaScript code, now it is used (and abused) everywhere in the app. I though it was a big PITA when I joined the team but I see the value now and I think the .NET ecosystem is lacking some standart way of doing this. Yes, there are exceptions, but we have a some states which are not "failured" per se and the Result library fits this well. The system is not perfect because the set of predefined result codes cannot be used everywhere and I am constantly thinking about how it could be improved. Does anyone have a clean way to do error handling in C#?


Errors are wrong.

Or, to put it more clearly: there are no errors, only conditions that you dislike. It's better to not burden your programming with your emotional shortcomings, and treat all conditions that you may encounter on an equal footing.

You try to open a file; the file may or may not exist, and both cases are equally likely and you get to decide what your program does in each case. No need to attach an emotionally charged label like "error" in one of the two cases of the conditional. Or worse, as some emotional fanatics do, to bend an otherwise clean programming language by adding features (e.g., exceptions) that help support your sentimental disposition.


There's also a performance trade-off. Collecting contexts and stack traces has a cost. Usually you could assume that errors are rare, so the cold path can be expensive. But not always: if you have logic such as "try A, and if it fails, try B", the error handling may suddenly be on the hot path.


This is actually why I dislike Python so much. The "try first, ask forgiveness later" truism has infused itself into the core language design and surrounding community. The end result is unexceptional exceptions and the presence of a dozen "warm" paths.


That's not a Pythonic thing. There are whole classes of problems where you'd have time-of-check vs time-of-use (TOCTTOU) bugs/vulnerabilities if you tried to do it differently.


That's the other thing I hate about Python, the concept of "Pythonic". It's literally the only language I have ever used where people will tell me my arguments are wrong because of (for lack of better words) a code of ethics.

Please don't take this as an insult, but I don't care what's Pythonic. I care what the community does, and they default to failing fast (to a fault, in my own opinion).


By "not Pythonic" I've meant that this issue is not unique to Python. It's not a problem created by some Python dogma. It's a fundamental issue affecting all languages, even those that don't have a catchy slogan for it. In Rust and C you also need to "try first, ask forgiveness later".

For example, you can never be sure whether you can open a file until you actually open the file and it doesn't fail. If you tried to check if the file exists and has correct permissions before opening it, you'd have possibility of a race condition. So the correct approach is to try the operation head first and handle the failure, instead of trying to somehow gracefully avoid creating an "exceptional" situation.


My bad! Text is funny sometimes, isn't it?

I agree with your basic argument here and you've convinced me to some extent. I wanted to dredge up some examples to highlight inherent flaws in Python's error handling ergonomics, but I was pleasantly surprised by new language features that made the experience much more enjoyable (the with ... as keywords, in particular).

I will say this though: try/except still sucks, in my opinion. I much prefer languages whose standard libraries utilize monads/callbacks for error handling, because it offloads much of the work/documentation onto the type system, as opposed to languages/frameworks which prefer error handling as part of a try/catch statement.

Monad Example (Scala documentation - Try monad [1] ):

  import scala.util.{Try, Success, Failure}

  def divide: Try[Int] = {
    val dividend = Try(Console.readLine("Enter an Int that you'd like to divide:\n").toInt)
    val divisor = Try(Console.readLine("Enter an Int that you'd like to divide by:\n").toInt)
    val problem = dividend.flatMap(x => divisor.map(y => x/y))
    problem match {
      case Success(v) =>
        println("Result of " + dividend.get + "/"+ divisor.get +" is: " + v)
        Success(v)
      case Failure(e) =>
        println("You must've divided by zero or entered something that's not an Int. Try again!")
        println("Info from the exception: " + e.getMessage)
        divide
    }
  }
Callback Example (NodeJS documentation - fs.readFile function [2] ):

  fs.readFile('/etc/passwd', (err, data) => {
    if (err) throw err;
    console.log(data);
  });
[1]: https://www.scala-lang.org/api/2.9.3/scala/util/Try.html [2]: https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_c...


I assume this is possible in Rust, so someone please help me understand: why not just print the stack trace?

In my opinion, if you suffer a sufficiently fatal error, you should just crash outright and log the trace + error message. Barring that luxury, you should log the trace + error message, then attempt recovery.

That seems to be what unwrap here would do on its own, so why would that be bad practice (assuming the error was fatal in the first place?). Am I just misunderstanding something fundamental?


Stack traces are not the best context for operational debugging.

a) stack traces generally don't capture parameters

b) stack tracers can be totally desynchronized with intent flow

c) stack traces expose unnecessary internal noise

Compare:

    Traceback (most recent call last):
       File "/var/prod/deadbeef/models/sql/peers.py", line 1337, in tx
         db.execute()
       File "/var/prod/deadbeef/models/sql/session.py", line 1337, in session
         tx(peers.add(a, b))
       File "/var/prod/deadbeef/models/__init__.py", line 1337, in AddBGPPeer
         session(peers, AddPeers(a, b))
       File "/var/prod/deadbeef/views.py", line 1337, in _view_add_peer
         AddBGPPeer(user, other)
       File "/var/prod/deadbeef/web.py", line 1337, in handle
         _view_add_peer(ctx)
    Exception: DB connector returned: unique constraint failed
to:

    Failed to add peer: models.AddBGPPeer(204480, 204480): insert peer: tx aborted: unique constraint failed
The first requires further logging and code reading to understand what might've caused the error. The latter immediately gives you information you need to understand what happened (the validation layer didn't catch an invalid self-reference, causing a database UNIQUE constraint to fail, and the database code caller didn't gracefully translate this error either).

I use the following pattern in Go very often:

    foo, err := m.AddBGPPeer(a, b, secret, list)
    if err != nil {
        pretty := list.StringSeparated(", ")
        return nil, fmt.Errorf("AddBGPPeer(%q, %q, _, %s): %w", a, b, pretty, err)
    }
The fact that Go error handling is just bog standard Go code allows for some quite complex generation of error messages/hints/contexts that couldn't be caught automatically from a stacktrace, or would have some awkward syntax/semantics with more complex syntactic sugar.


In a larger codebase, a combination of both would be more helpful. With the last one, you're not getting the same view as with the latter, and in a large codebase where the error could be triggered from many directions, it would be better to have the first example, at least in my experience.

    Traceback (most recent call last):
       File "/var/prod/deadbeef/models/sql/peers.py", line 1337, in tx
         db.execute()
       File "/var/prod/deadbeef/models/sql/session.py", line 1337, in session
         tx(peers.add(a, b))
       File "/var/prod/deadbeef/models/__init__.py", line 1337, in AddBGPPeer
         session(peers, AddPeers(a, b))
       File "/var/prod/deadbeef/views.py", line 1337, in _view_add_peer
         AddBGPPeer(user, other)
       File "/var/prod/deadbeef/web.py", line 1337, in handle
         _view_add_peer(ctx)
    Exception: Failed to add peer: models.AddBGPPeer(204480, 204480): insert peer: tx aborted: unique constraint failed


Absolutely. I'm not saying exceptions are bad and harmful, I'm saying that exceptions aren't an automatic solution to good error handling.


I don't understand the mindset you have to be in, in order to prefer the second error example to the first


Production outage at 2am.


The mindset is that an program output like this in the event of a user input error is not good:

    Cup
    60 skins in DB
    104 drivers in DB
    thread 'main' panicked at 'Could not find car folder or its skins: acrl_poorsche_911_gt3_cup_2017', src\main.rs:104:17
    stack backtrace:
       0:     0x7ff72a06deca - std::backtrace_rs::backtrace::dbghelp::trace
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\..\..\backtrace\src\backtrace\dbghelp.rs:98
       1:     0x7ff72a06deca - std::backtrace_rs::backtrace::trace_unsynchronized
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\..\..\backtrace\src\backtrace\mod.rs:66
       2:     0x7ff72a06deca - std::sys_common::backtrace::_print_fmt
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\sys_common\backtrace.rs:79
       3:     0x7ff72a06deca - std::sys_common::backtrace::_print::{{impl}}::fmt
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\sys_common\backtrace.rs:58
       4:     0x7ff729fb93bb - core::fmt::write
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\core\src\fmt\mod.rs:1080
       5:     0x7ff72a06c9e8 - std::io::Write::write_fmt<std::sys::windows::stdio::Stderr>
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\io\mod.rs:1516
       6:     0x7ff72a06bd18 - std::sys_common::backtrace::_print
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\sys_common\backtrace.rs:61
       7:     0x7ff72a06bd18 - std::sys_common::backtrace::print
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\sys_common\backtrace.rs:48
       8:     0x7ff72a06bd18 - std::panicking::default_hook::{{closure}}
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\panicking.rs:206
       9:     0x7ff72a06bd18 - std::panicking::default_hook
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\panicking.rs:227
      10:     0x7ff72a06bd18 - std::panicking::rust_panic_with_hook
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\panicking.rs:577
      11:     0x7ff72a073ab5 - std::panicking::begin_panic_handler::{{closure}}
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\panicking.rs:484
      12:     0x7ff72a073a77 - std::sys_common::backtrace::__rust_end_short_backtrace<closure-0,!>
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\sys_common\backtrace.rs:153
      13:     0x7ff72a073a1f - std::panicking::begin_panic_handler
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\panicking.rs:483
      14:     0x7ff72a0739e0 - std::panicking::begin_panic_fmt
              at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4\/library\std\src\panicking.rs:437
      15:     0x7ff729f3501e - __ImageBase
      16:     0x7ff729f3f9eb - __ImageBase
      17:     0x7ff729f51486 - main
      18:     0x7ff729f40277 - main
      19:     0x7ff72a0885f0 - invoke_main
              at d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:78
      20:     0x7ff72a0885f0 - __scrt_common_main_seh
              at d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
      21:     0x7ffeb18b7034 - BaseThreadInitThunk
      22:     0x7ffeb1e9cec1 - RtlUserThreadStart
That stack track is completely worthless. It tells you literally nothing. Worse, is that while the actual problem is printed, it gets lost with the mess of noise after it. Whereas an error output like this is clearer:

    Cup
    60 skins in DB
    104 drivers in DB
    Error: Failed to verify series data
    Caused by: Could not find car folder or its skins: acrl_poorsche_911_gt3_cup_2017
The core error is that a car or skin folder was missing, and that error caused verification to fail. That's what should be printed, because that's what is relevant.


All this tells me is that Rust gives crappy useless stack traces that include noise of functions handling the panic itself and the creation of the stack trace, and this has poisoned your view of stack traces.

Why can't Rust produce much more readable, concise, and useful stack traces like Python, that include the function name, file name, and line number of each step of the stack?


The majority of those lines are including the function name, the file name, and the line number.


Look closer.

The only ones with line numbers are library code handling the panic. None of the actual user code has line numbers or even the file name, just a function name.


src\main.rs:104:17 is.


Oh, so you're right. There it is right at the very top of the error.

So why couldn't the trace include that in that in the body?


Because the backtrace is optional, the formatting is consistent with it and without it; you get that line no matter what.


having worked with both cases I'd take the backtrace every single day


But that backtrace doesn't actually tell you what function the error occurred in. The function stack would be this (discarding everything outside of my code):

    1: ImageData::verify_and_update - src\main.rs:104
    2: run - src\main.rs:653
    3: main - src\main.rs:673
That's why the backtrace is worthless here. It doesn't actually tell you where you were in the chain of function calls. In this case, it's relatively simple, so just knowing the line does help. But in a more complex application you could end up in a scenario where a function is called in multiple places, with different call stacks, and you don't know how you even got there because things were inlined and the backtrace doesn't tell you.


I don't know how rust's error libraries work but I definitely don't have this issue in C++ - the top of the backtrace is pretty much always where the error is


I don't actually know what an intent flow is! I tried to Google around, but all I can seem to find are proprietary keyword mishmashes used for digital payment systems. I'm guessing you're talking about, like, intent in the context of a state machine? Set me straight here if I misunderstand.

I see the validity of what you're saying, but only in the context of insufficiently verbose third-party error messages. There's no reason that the exception message could not have been more detailed.

In my own projects, I like to design the functions that constitute the "plumbing" in such a way that they rethrow errors with more descriptive errors. We're venturing out into the realm of practice from that of theory, so this is just me sharing for the hell of it, but here's how that looks:

    // Helper function meant to be reused in many function definitions
    function throwIfFunctionParameterUndefined(functionName, parameterName, value){
        if (value === undefined)
            throw new TypeError(`${functionName} invoked with undefined parameter ${parameterName}`)
    }

    function doThing(paramA, paramB, optionalParam){
        // Assert and throw for things we can guarantee in a meaningful way
        // Since this is plumbing, we trust that higher level functions will catch if this is recoverable
        const throwIfParameterUndefined = (paramName, value) => throwIfFunctionParameterUndefined('doThing', paramName, value)
        throwIfParameterUndefined('paramA', paramA)
        throwIfParameterUndefined('paramB', paramB)

        // Rethrow errors from third party libraries
        try {
            thirdPartyThing()
        } catch (err){
            err.message = 'thirdPartyThing invoked by doThing failed with message:\n' + err.message
            throw err
        }
    }


> I don't actually know what an intent flow is!

Sorry, I might've made that up :).

What I meant is that stracktraces do not necessarily reflect the actual code path that the error was caused by (eg. with deferals to internal work queues), and even if they do, they tend to have terrible SNR (too much technicalities of how your code is structured, not enough information about the data that caused the error in the first place).


Not every error is fatal, and unwrap can make trivial errors fatal.


I'm willing to accept that argument for unwrap, makes sense if you're dealing with a situation where multiple errors can apply.

Outside of unwrap, which actually crashes out, is it not possible to access the stack trace? Forgive my ignorance, I've never worked in Rust nor any other on-the-metal languages before!


In Rust there generally isn't a stack trace, because you generally can know where the error comes from. What you get for an error depends on what is giving you the error. Generally you get an enum with the general error type, and maybe a way to access the specific error.


> In Rust there generally isn't a stack trace, because you generally can know where the error comes from.

I don't understand. Does that mean that there's sometimes a stack trace? Why would it matter if you had other ways to know where the error comes from? Surely you could say the same thing for the majority of programming languages.

Sure, Rust can wrap errors via the type system, but I don't think that's a compiler-level guarantee. Maybe it's just a language design decision, then?

> Generally you get an enum with the general error type, and maybe a way to access the specific error.

That seems odd to me! Is the presence of a pointer to an error object something that's implementation-specific? If so, is it not typically done in the standard library? As far as you can speculate, what's the design justification for that?


> Does that mean that there's sometimes a stack trace?

Yes. There's a bunch of different bits that come together to cause this, but the core of it is roughly that keeping stack traces adds a cost that not everyone is willing to pay at all times. Rust is trying to figure out how to best accomplish good error handling in an environment that doesn't provide easy answers like "just do it like x at all times."

> Is the presence of a pointer to an error object something that's implementation-specific?

To use the Go parlance, errors are values in Rust. They're not inherently special. There is an interface you can implement if you want interoperability, but Rust only gives you the ability to interoperate, it cannot force you to.


Error traces are generally only on panic. Rust does not have exceptions, and errors must generally be handled where they can occur.

Error context is implementation-specific. This is partially because Rust is a young language, and the best-practice API hasn't really been figured out yet.


I think the other replies are a bit confusing. unwrap causes a "panic" - panics in rust are similar to python exceptions, but it's not catchable. (there actually is a way to catch panics because that's how the stacktrace printing is implemented, but you usually only use it if you're making a custom allocator or runtime or some other voodoo magic)

So if you want your "exception" to be able to be handled by the caller, you have to indicate it otherwise, usually with enums.


The way I frame error handling is there are three kinds of errors:

The first kind of error is the "programming oops" error--division by 0, null pointer dereference are obvious examples. These kinds of errors potentially arise from almost everywhere, but you're never expecting to handle them except at the very highest catch-all level. Consequently, you generally want to have maximum-detail error logging of the entire stack trace and perhaps the environment around the cause of the failed exception to understand what wrong.

The second case of error is the expected error class--I/O exceptions and file-not-found are the prototypical examples here. Generally, this will be handled immediately, and you may be unwilling to pay the high performance penalty of constructing the exception context if you are going to throw that context away on the very next line. Instead, your want your exception context to be no more expensive than an error code.

The third and final class of error is application errors that combine both properties. You want contextual information for errors, explaining not just what went wrong but also the sequence of processing decisions that led you to do that wrong step (e.g., pointing to configuration file entries). A stack trace isn't necessarily the worst default for context, but it's usually far from the best information. The application, when handling the error, may well want to poke heavily at the error details to display it in more user-friendly ways.

As it exists at present, most error handling systems focus on either the first or second class of errors. More rarely, they try to handle both of these classes (Rust's Result/panic divide is an example here). But there is very little support for the third class, with the closest alternative being providing context via chaining of exceptions.


IMO Erlang/Elixir does it best with the "let it crash" mentality. Once you internalize and truly understand how it works, you won't want to do error handling in any other way. The tldr; handle errors you can handle, let everything else crash and rely on the runtime to bring you back online.

The zen of erlang (https://ferd.ca/the-zen-of-erlang.html) goes more in depth if you're curious.


The conflict here is situations, like in Rust, where you don't have that runtime in the first place, so you cannot do this.

Rust used to be much more Erlang-like in this regard back in the days when it had such a runtime, but when the runtime was removed, so was the ability to work like this.


Yeah, usually the supervisor just restarts what it should, and the error is logged. That is enough for me, to be honest.


Here is my solution:

    use std::fs;
    fn main() -> Result<(), String> {
       let s = "a.rs";
       match fs::read_to_string(s) {
          Ok(v) => print!("{}", v),
          Err(v) => Err(format!("{} {}", s, v))?
       }
       Ok(())
    }
or you can use `map_err`:

    let out_s = fs::read_to_string(in_s).map_err(|e|
       format!("{} {}", in_s, e)
    )?;


Reading over the comments here makes me chuckle a bit because it proves the author's point: "Error handling is hard". I don't think any silver bullet solution has been invented yet.

I'd like to see error handling supported in a transactional way, which at least would roll things back to a clean state before the error happened. Of course if you performed an operation which cannot be rolled back, then so much for my great idea.


I feel like sometimes software could benefit from a better conceptual differentiation between failures and errors.

Failures happen, and robust software should deal with them in a predictable manner. Not all software needs to be able to recover from failures automatically, but they should remain in a consistent state so that an operator can fix the situation, allowing the system to recover. A database system unexpectedly going down is a failure, and a system that expects it will never happen can't be considered correct.

Errors are the result of a mistake: Either there's an invalid input from the user, or a bug has caused data corruption. Errors can be detected with defensive coding (eg. by validating input), but often systems can't recover from errors, so they to be communicated to the user. You'll want systems to abort their current task if they encounter errors, as the state will be undefined, and continuing may make the situation worse.


In Rust, we call these "recoverable" vs "unrecoverable" errors, and use Result and panic for them, respectively.


I don't get all the exception hate.

Exceptions are great when you are prototyping something. You will not have to bother about the very very very edge case. This is important when you have HOFs.

For robust production software though, you need something like result type or checked exceptions.


It's probably because too many of us have written too many prototypes that turn into production software


My problem with them is that they masquerade as a real error handling solution.

Rust's panic is better since it makes it clear that you are just completely giving up and that the scenario is not being handled.


I am fine with Forth's exception handling, or even Lisp's and Factor's.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: