7 ms·
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.
by theamk 6y ago
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.
- Forge36 6y ago+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.
- adimitrov 6y agoI 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...
- joseluisq 6y agoYeah, 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.
- indiv0 6y agoI 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.
- jcranmer 6y ago> 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.
- ddek 6y agoAs 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-checked-exceptions https://stackoverflow.com/questions/613954/the-case-against-...
- smichel17 6y agoWhat 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.
- smichel17 6y agoReply to self: barrkel's comment upthread describes the premises I was working off of quite elegantly: https://news.ycombinator.com/item?id=25260631 https://news.ycombinator.com/item?id=25260631
- MaxBarraclough 6y agoYou 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=36693 https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...
- alquemist 6y agoI 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?
- MaxBarraclough 6y agoI 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.
- barrkel 6y agoSpurious 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.
- karatinversion 6y ago> 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.
- karatinversion 6y agoMy 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.
- renox 6y agoWalter 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 https://vale.dev/blog/next-steps-raii
- diggan 6y agoI 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.
- Macha 6y agoThe 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/fasterxml/jackson/databind/ObjectMapper.html#readValue(java.lang.String,%20java.lang.Class) https://fasterxml.github.io/jackson-databind/javadoc/2.7/com...
- indiv0 6y agoWouldn't you have that same issue in Rust though?
- Phlogistique 6y agoWell, same issue and same solution (see other answers), except that in Rust, instead of `RuntimeException` it's named `panic`.
- adrianmsmith 6y agoYou 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); }
- etripe 6y agoI 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.
- chrismorgan 6y agoFor 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.
- Huggernaut 6y agoI agree with this. I have tried briefly to make use of Result and Option types from https://github.com/cognitedata/Expression 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.
- FridgeSeal 6y agoAfter 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.