9 ms·
Exceptions should be Exceptional (2016)
- virmundi 7y agoI’ve taken Go’s approach to error handling in new typescript projects. I want to force my team into thinking about IO errors. I want failures to be part of the domain since stack traces only kinda work in Node. We will only throw exceptions if someone didn’t program for bulls properly. Everything else is a Failure instance passed up the call chain. Annoying, but our testing and code-thoughtfulness have gone up.
- bitwize 7y agoGo handles it shitty, like the past five decades of computer science and PL design never happened. For imperative languages, exceptions are perfectly fine as a way to encode failure as part of the domain of operations. If you're in functional fantasy land, the correct approach is to use a Maybe monad or, better yet, an Either monad. But Go supports neither of these and that's one of many reasons why it sucks for modern development. In TypeScript you can do better and, for the sake of others who have to maintain your code, you should do better.
- pixelrevision 7y agoThe biggest mistake I think go made was not to include codes or domains. The error you get back is... a string. So you have to write tons of if statements just to return a bunch of errors you can’t handle.
- donatj 7y agoError is an interface. The default one may be a string but you can implement your open error with whatever extras you would like.
- CameronNemo 7y agoRight but then you need to use errors.as to match against it, rather than being able to do pattern matching. And there is no inheritance, no ability to generalize errors into a group.
- donatj 7y agoYou can totally just type assert your custom error type to check for it. If it's the only type of error your method returns you don't even need to, just define your return type as your custom error type.
- QuinnWilton 7y ago> Go handles it shitty, like the past five decades of computer science and PL design never happened. This has generally been my complaint with Go. Go is... fine. It offers a lot of features that seem revolutionary if you've never seen them before, but mostly feel like someone took the best parts of every language, and then implemented the least interesting aspects of those features. - Interfaces are like typeclasses, but without actually offering any sort of higher-kinded polymorphism. - Goroutines are kind of like Erlang processes, but without any notion of supervision and with shared mutable state. - The error handling feels like it's trying to emulate an Either monad, but in a way that doesn't actually allow for the compositional properties that make an Either monad useful. I know that Go's goal was never to innovate on language design, but I feel like it ignored a lot of low-hanging fruit in modern language design. Obviously you can do worse than coding in Go, but for everything Go is good at, there's likely an even better tool for the job. I'm not convinced that there's any domain where Go is the best choice available, but it's definitely average at most jobs, and maybe that's where it finds its niche. Edited: I had missed the word "higher-kinded" in front of polymorphism. Thanks donatj!
- donatj 7y ago> Interfaces are like typeclasses, but without actually offering any sort of polymorphism Can you explain what you mean by this? I certainly use them in a manner I would consider polymorphic.
- QuinnWilton 7y agoYou're completely right; I accidentally dropped the word "higher-kinded" in front of polymorphism. Go's interfaces provide structural typing, which offers polymorphism over values. Typeclasses in comparison, offer polymorphism over types themselves. The distinction is subtle, but it drastically limits the sorts of abstractions that you can write with Go. In Go's defence, this is a limitation that even functional languages like F# and Elm face too. This sort of polymorphism is what makes ideas like monads and functors so powerful in a language like Haskell, and what causes those same ideas to lose some of their lustre in a less expressive language. Unfortunately it's really difficult to find good articles on higher-kinded types, since anything written about them already assumes a ton of knowledge about the subject. This StackOverflow thread has some good insight though: https://stackoverflow.com/questions/21170493/when-are-higher-kinded-types-useful https://stackoverflow.com/questions/21170493/when-are-higher... It's entirely likely that in the real world these sorts of limitations don't actually matter, but my general frustration with Go stems from the fact that it's so close to being so much better than it is, and it saddens me that we as an industry chose to be so excited by something that's a tiny step forward, when there exist so many other options that are entire leaps and bounds forward.
- danenania 7y agoUsing a Maybe type sounds a bit better, but not really that different than how Go does it in practice. The key decision is whether to throw errors or return them. Go's approach and the Maybe type are both variations on returning them. The problem with returning them is it's tedious and you end up with error checking boilerplate everywhere. The problem with throwing them is they're easy to ignore, and also you never know whether or not a function is capable of throwing an error, or what types of errors it can throw. In conclusion, error handling sucks no matter how you do it :-/
- smabie 7y agoYeah, it does, but using monads to fold over errors is a lot nicer than what you have to do with Go.
- ramzeus 7y agoMy experience with Go have been very positive. Once you get over the initial irritation of the error handling, I think it works perfectly fine. I have never been so productive as I can be when programming in Go. I’m sure there are technically better alternatives, but it has a very good balance between features, speed, standard library and multi platform support. At least for the backend server tasks I use it for. My biggest complaint has more to do with the new module system and tooling. The (not so) new Go Language Server is still very buggy and almost unusable in my opinion.
- jrimbault 7y agoTypeScript is equipped for having and handling option (maybe) types (`T | null`) or result (either) types (`T | Error`), why not handle errors in a proper way ? A way the compiler ensures you handle all cases in every cases ? If you do it the go way, I assume every fallible functions returns something like `[T, Error]` ? With no guarantees the error is actually checked.
- Avi-D-coder 7y agoI disagree with you, but your comment should not have been down voted so I up voted.
- fetbaffe 7y agoI used to be in exception camp, but I have lately turned to the anti-exception camp. Exceptions have a nasty way of polluting your architecture, they live orthogonal to the rest of the system but still part of the API and have a tendency to mess with every part in every layer & module. With exceptions is it easy to end up in a situation where your model code changes UI behavior. Let say your model throws ItemNotFoundExcpetion, are you going to catch that in your global handler & return a 404? The proper way is probably to catch it in your controller & rethrow a ResourceNotFoundException to the UI. Thus you need to do that for every exception, catch & rethrow. And not only you need to translate exceptions between layers & you probably want to translate exceptions between modules. The number of exception classes in your project starts to explode. You have to create hierarchies and groups of exceptions. And many of them are just copies of others. In a PHP project I experimented with a similar approach as Go, unfortunately PHP does not a have good tuple type support. You can return an array from a function, however the items will be untyped so you have to rely on phpdoc. What I did instead was to use an out variable for the error. function parseInt(string $value, ?string &$error): ?int; $int = parseInt('10', $error); if ($int === null) { echo $error; } Worked pretty okay. Still undecided if this is a good approach.
- jrimbault 7y agoIt's a classic approach in some C codebases (Sun I think used it extensively ?). My gripe with it is (only for php), it's not visible from the calling side that we are passing a pointer.
- fetbaffe 7y agoAgree, lets hope this rfc gets approved. https://wiki.php.net/rfc/explicit_send_by_ref https://wiki.php.net/rfc/explicit_send_by_ref https://externals.io/message/108699 https://externals.io/message/108699
- iamaelephant 7y agoCan't we all just agree that Maybe/Optional types are actually a great idea that generalizes all of this at minimal cost?
- smabie 7y agoI don't think options really serve the same purpose as exceptions. If you have are just passing up None or whatever up the call stack over and over again, you probably should just be using an exception. If not, then options are fine. I use options for normal behavior (a missing file, for example) that perhaps can be corrected or handled in a different way. I throw an exception if I want to just crash the program. I rarely ever try catching exceptions, except in maybe some top level driver function. Oh and to all the programmers out there who wrote the code I'm currently dealing with: please don't use exceptions for flow control, it's a terrible terrible idea.
- mrkeen 7y ago> please don't use exceptions for flow control That's kind of the problem though. Exceptions are flow control.
- rumanator 7y ago> Exceptions are flow control. ...for exceptional events. That means those events that are very far from the happy path and need to be recovered by unwinding down multiple function calls until a safe point to recover gracefully is reached. These are events such as failing to allocate memory.
- klntsky 7y agoThe cost is significant: introducing one more level of indirection (for tagging) is not cheap. However, Maybe can be implemented without tagging, see[0] for example. [0] https://hackage.haskell.org/package/unpacked-maybe https://hackage.haskell.org/package/unpacked-maybe
- 7y ago
- wwweston 7y agoAn exceptional condition is arguably anything that means some call in your program can't do its job. Passing error conditions up the call stack to the control layer that displays and/or logs a relevant message is a pain. Exceptions make it less painful. Catch blocks should be used sparingly, sure. If you're using them close to where the error is thrown then maybe you should consider an error code-oriented approach. But catch blocks in controller code (or better yet, meta-controller code)? Sure.
- cmroanirgo 7y agoThe problem is that one person might think an exception is acceptable for OpenFile, whereas another thinks it's not. It's this ambiguity that makes for all sorts of conflicting ideas and recommendations. Fun Fact: the Windows API uses HRESULT to return success/failure of a particular call. The COM API uses the same. VB6 was the first platform by Microsoft to implement exceptions throughout the code (that I'm aware of)... and beneath it, it used COM. So that means that VB6 generated an exception whenever a COM status code returned E_FAIL or similar, and didn't if it was classed as SUCCESS (eg S_OK, S_FALSE, etc). The original .NET implementors clearly used the VB6 model when developing the codebase (it was nearly a one-to-one match from VB6 to VB.NET) and very easy to port over. That said, when I read the article, "Exceptions are the primary means of reporting errors in Frameworks" vs "Do not use exceptions for the normal flow of control, if possible" I see this never ending problem appear. Most things we do in our programs use Frameworks, which throw exceptions, which may or may not actually be exceptional for the case of the calling function. Nevertheless, it must be handled. However, back in the C++ Win API and COM world, you could easily use: `HRESULT hr= SomeFunc(); if (FAILED(hr)) return hr;` to bubble up a failure code, or if you handled it in other ways, you can safely ignore the return code and continue on. In the .NET world exceptions must always be handled otherwise they ripple all the way up and end up as a crashed application. Of course, some think this is a good design principle to have it so: Others are not so convinced. And so, the debate continues.
- acqq 7y agoExactly. I am certainly in the "exceptions should be really exceptional" camp and therefore I didn't "agree" with Python's open function: https://docs.python.org/3/library/functions.html#open https://docs.python.org/3/library/functions.html#open "Open file and return a corresponding file object. If the file cannot be opened, an OSError is raised." in the sense that in a program that is not only a few lines, I would prefer such kinds of library functions not to produce exceptions, thank you very much, so I had to wrap all such. But it is not optimal not being able to directly use language primitives as much as possible. On another side, writing really short programs gets to be faster: if I write only a few lines, having an "implicit" error handling shortens the code. And that's what I think is the main reason for many ills and misunderstanding in many popular languages: the practices that look good for small examples (or in the books) aren't the same as those which work well for large projects. Python's not detecting the typos in the variable names in compile time is also a typical bias towards the short programs. For large projects detecting as much at compile time and having only very exceptional exceptions and unsurprising control flow (visible reading ifs, and with a guarantee that there can't be "landings from anywhere" like in exceptions) is of huge advantage.
- scotth 7y agoDon't .NET dictionaries throw when a requested key isn't found?
- alkonaut 7y agoYes, for the indexer scenario dict[key] that is an exceptional failure (equivalent to indexing list[index] wher index >= length). The method to get the value of a key only if the key is in the dictionary is TryGet... I think this design is the best one. It lets you have the program blow up quickly in all cases where the presence of the key is a requirement for the program to be in a valid state (which is common). Otherwise you'd be doing x = dict[key] if (x == null) throw("Unexpected this is. And unfortunate");
- ailideex 7y agoChecked access to c++ containers also throw ... https://en.cppreference.com/w/cpp/container/map/at https://en.cppreference.com/w/cpp/container/map/at
- SigmundA 7y agoYes and probably make the most sense as .Net has no "undefined" value. If the dictionary has a the key with a null value it returns null, but no key throws. With generic dictionaries (which everyone uses now days) you have TryGetValue which is a little weird since it uses a out parameter but pretty natural after c# 7 inline out params.
- 7777fps 7y agoI often think it's a shame there isn't a "GetOrDefault" for dictionaries. An extension like > dict<k,v>.GetOrDefault(k key,v valueIfNotPresent) Would be really helpful. It's easy enough to write your own extension to do this, but something built in would be the kind of nice to have method that has been creeping into .net.
- SigmundA 7y agoYes I agree, ImmutableDictionary has GetValueOrDefault, GetOrAdd is nice on ConcurrentDictionary and FirstOrDefault is nice in Linq.
- leggomylibro 7y agoI wonder why callbacks aren't a more popular way for languages to handle exceptions. What if each type of exception registered a weak link to a function with a name like `IOExceptionHandler` which prints a noisy error if it isn't overridden with other behavior? I guess it would be harder to use a single exception callback if you had a lot of similar "try/catch" blocks in different areas of your code, and you'd still probably need to collect some sort of stack trace, but it might work out alright if you go with this article's advice and recommend that people keep exceptions out of their program's normal control flow. I dunno, maybe it's a dumb idea and I've just been spending too much time with microcontrollers lately. They use interrupt callbacks for all kinds of hardware triggers and software events, and I kind of like how you can compartmentalize error handling into a few predictable event handlers. That Java stack trace image at the end of the article also brings back some memories...but I'm feeling much better these days.
- lopsidedBrain 7y agoThat is sounds more appropriate for either terminating the program or resumable exceptions like division by zero. In languages with a try-catch construct (specially C++), the complexity of implementing exceptions has to do with stack unwinding. This is where you don't want to restart in an outer context, but want to guarantee cleanup for the call frames you are abandoning.
- leggomylibro 7y agoHm. Good point, I guess you couldn't easily clean up things like local memory allocations from a common exception handler, huh? Oh well, they can't all be winners.
- taeric 7y agoIf you think callbacks are nice, you should see the condition/restart system of common lisp...
- danjc 7y agoRelatedly, I'm a strong proponent of debugging with "break on all exceptions" enabled. It's very easy to miss bugs if you're not aware of exceptions being thrown but for this to be practical the code must only throw exceptions exceptionally or it's a nightmare to debug.
- harperlee 7y agoAnecdotally one of my stumbling blocks with clojure was how to deal with errors. The language style heavily pushes to just coding the happy path and using nil punning, instead of exceptions, but sometimes you need to signal errors, so some libraries use keywords for error codes - but now that we have spec the community seems to be shifting opinion to “exceptions are ok-ish if a result of not conforming”. Both error codes and nils make spec definitions ugly, thus all the “maybe not” opinion by Rick... it seems someehat overwhelming, to be honest.
- deleted 7y ago[deleted]
- speedplane 7y agoFact 1: There are always bugs or unforeseen events in software. Fact 2: When there is an unforeseen event, the developer did not consider what to do. Fact 3: Developers often deal with other people's buggy code. It's clear that exceptions aren't the perfect solution to anything, but given these 3 facts, exceptions are around to stay.
- dgellow 7y agoMaybe the issue is that "exception" is a really confusing name. Go has the keyword "panic" for errors that stop the current flow and unfold the stack, that makes it clear that you shouldn't use them for anything else than exceptional situations, and they can be "recovered" if they have to be handled explicitely. https://blog.golang.org/defer-panic-and-recover https://blog.golang.org/defer-panic-and-recover
- 7777fps 7y agoFrom my (admittedly limited) experience, Go has the worst story for error handling, where functions end up repeatedly checking for error return conditions from called functions. It feels trapped between "We don't want exceptions" without going full "Result<T> or Maybe<T>" functional style that might be more typical of something like elixir. But I didn't try Go for long and that itself was years ago, so perhaps modern Go has better paradigms.
- meddlepal 7y agoNope. Error handling in Go is still tedious.
- dgellow 7y agoI'm just mentioning the naming, not the general approach to error handling. Panic are roughly similar to what an "exceptional exception" is (stop control flow, unfold stack until being handled), but the naming is slightly better IMHO. I personally like Go's approach, but that's not for everybody (I'm also quite fond of the monadic approach though).
- pingyong 7y agoWhen your main argument for not using exceptions for every kind of error handling is performance instead of architecture, then maybe it is a sign that exceptions should simply be constructed to be more performant instead of giving people advice that isn't and can't ever be consistent. As the writer of a library (or just other utility functions), you simply cannot know whether something is an exceptional situation or not, because that is a property of the program and the environment, not the function. Being unable to parse a string to a number, or to create a socket, or to even allocate memory can all be exceptional or not exceptional depending on the context, and often you simply do not know the context this code will run in, or the code will actually run in multiple directly contradictory contexts.
- mratsim 7y agoI totally agree for things like streams or IO like file handling or authentication. It may be fatal at the IO layer but in a GUI you want a nice error displayed to the user and not a crash. However in some domains you don't want to pollute your return value with a Result or Error enum, for example a BigInt library addition or a Matrix addition would be much more convenient for scientists to use if it returned a BigInt or Matrix respectively.
- UK-Al05 7y agoWith bind style semantics its not that bad using result types for matrix or bigint.
- dgellow 7y agoAs a library writer you can let the caller the responsibility to determine if something is worth throwing an exception for or not. 1. return an error code 2. if the caller consider the issue to require an exception, they will do it themselves You can also have entrypoints to the library that will throw exceptions, and other versions that won't, and let the caller decide what they want to use. Though of course that's more work. Regarding the point of performances, that makes me think of https://news.ycombinator.com/item?id=22483028 https://news.ycombinator.com/item?id=22483028, discussing "Low-cost Deterministic C++ Exceptions for Embedded Systems". It would definitely be very interesting to see what is now possible if exceptions are cheaper.
- zorbash 7y agoFor Ruby there's this book, by the exceptional Avdi Grimm https://pragprog.com/book/ager/exceptional-ruby https://pragprog.com/book/ager/exceptional-ruby it's a fantastic resource on the subject.
- coding123 7y ago> So exceptions should be exceptional, unusual or rare, much like a asteroid strike!! That's a super huge misread of the text recommendation. I watched a live presentation directly from Jeffrey Richter at the bay area .net user group, oh god about 15 years ago when he described how he came up with this text. In fact it was a call to use exceptions a lot more than we used to, and to avoid error codes. So no, exceptions are not supposed to be rare like that. His main point was to throw if the method name was not achievable, except in specific situations where that breaks user expectations too much, like getting null from missing map entries.
- dkarl 7y ago"Exceptional" has no reasonable objective definition, especially not at the library level. When you try to open a file by name and it doesn't exist, is that exceptional? It depends on context. Whether something is "predictable and intentional" can even change with product evolution. It's a recipe for bikeshedding. This is a crazy criterion to try to make coding decisions by. Different applications need different trade-offs, and not every API will be suitable for all users. Standard libraries will make decisions according to what kind of applications the language targets. Libraries making other trade-offs can be written to cater to people with other needs. I get that some people are repeatedly encountering situations where they feel like exceptions are used in ways that reduce readability and performance, and they feel like this is a rule that could improve the situation by reducing the use of exceptions across the board, but please, apply your usual criteria of clarity, readability, and performance! If you believe that exceptions are the clearest way of expressing a certain bit of logic in your language and codebase, and that they are acceptably performant, don't get distracted from that by bottomless, ultimately insubstantial arguments about what it means to be "exceptional." Edit: Just wanted to add that I really enjoy the more expressive type systems that make it ergonomic to use return values in a lot of situations where exceptions are otherwise the most readable choice.