12 ms·
My problem with exceptions isn't so much exceptions themselves but the way they're used. The way I see it, exceptions should only be used for things that are i
by ravenstine 7y ago
My problem with exceptions isn't so much exceptions themselves but the way they're used.
The way I see it, exceptions should only be used for things that are irreconcilable, which most of the time is interpreter errors(e.g. undefined is not a function). In other words, I don't think it's that common that custom exceptions are needed outside of assertions to prevent the developer from doing something stupid. If you aren't using types, raising an exception can be helpful to provide feedback and useful information when data isn't formatted correctly.
Exceptions suck when they're used for problems that aren't actual problems. For example, if a query is made to an API for a record and that record isn't found, the SDK wrapping that API would raise a "Record not found" exception. What the hell? A record not being found is a normal thing! I've seen this kind of thing all over the place, and I recently had to write some application code to work around one of these useless exceptions. They literally tell you nothing and there's no way to solve them without catching/rescuing them. A null value, a plain "error" object, or an error argument in a callback would have been sufficient. If I need an exception to be raised for this kind of thing, I'll do it myself.
- shadowgovt 7y agoI find the mindset that exceptions are the right way to handle ordinary scenarios symptomatic of what I call "happy path coding:" assuming that the stars are always perfectly aligned for your program to work, and nothing ever goes wrong, and if it does it's someone else's problem. It's a fine approach for prototyping but we should be highly suspicious of it for code people rely upon to work. As complexity approaches infinity, the odds of the system being "happy path"-aligned approach zero. Network-relevant programming (such as web UIs), in particular, really highlights this fact (since any given network request is always allowed to fail, and "user closes their laptop or mobile device, moves somewhere else, and resumes work on an entirely different physical network" has gone from a rare occurrence to an extremely common one in the past two decades).
- AstralStorm 7y agoOn the other hand, do you really want to bubble error results through layers of network stack and parsers, when handling is almost always the same?
- yyhhsj0521 7y agoThe thing is that exception is as close as we get to pattern matching in many languages. When a language has even the most basic Either/Maybe/Option, you start to see fewer exceptions being thrown around.
- baby 7y agoActually, I’d argue that exception (that are caught) should only be used for recoverable errors. If you can’t recover, then you should crash.
- munificent 7y agoThe great thing about exceptions is that they implement both of these behaviors with a single mechanism. For recoverable errors, you catch. For unrecoverable ones, you don't. The exception throwing mechanism unwinds the entire stack and crashes the app with a helpful stack trace.
- baby 7y agoTrue!
- seanmcdirmid 7y agoIf you expected the record to be found, the failure of finding it is an exception. If it was an open question, then it isn’t. This is why API generally have checks that allow developers to avoid that failure if the failure is expected (eg Record.exists). What you are suggesting is simply a reverse order (check after vs check before). I think check before leads to a much cleaner API than one that dumps a null or failure code instead.
- takeda 7y agoIt all depends on context. I believe that's why python for example throws an exception if given key doesn't exist when you use `dictionary[key]` but also gives you option to call `dictionary.get(key, [value])` which returns `None` or `value` (if specified) if the `key` doesn't exist.
- samatman 7y agoThat one bites me when I've been away from Python for awhile. I prefer 'open' maps/dicts, where every key is valid and any unassigned value is None/nil. I don't consider looking for an invalid key exceptional, basically, so I wish the syntaxes were reversed, so I could use the more succinct form for (my) more common case.
- loopz 7y agoGolang maps returns the value type's zero value in case the key doesn't exist: https://blog.golang.org/go-maps-in-action https://blog.golang.org/go-maps-in-action The idiomatic way here being that the zero value should be a sane default for that value. You are then free to create your own types ontop of that, but the defaults are often in the ballpark of where you want to be unless you have specific needs in your context.
- r00fus 7y ago> If you expected the record to be found, the failure of finding it is an exception. This is not always the case. In fact, for query APIs I do not expect it at all. Sometimes I'm doing something similar to a UPSERT operation with more nuanced behavior, then returning no existing entry on a preceding SELECT query could be the 99% use case.
- wvenable 7y agoAbsolutely. Whether a method throws or not signals the intent of the code. In .NET, there are plenty of throwing and non-throwing versions of various methods. For example, there are both Parse() and TryParse() and also First() and FirstOrDefault() in Linq. Which method you use signals to the reader the expected result. If you're using Parse() then you're expecting the parse to succeed; Perhaps the data comes from a data file and if it's not an integer then something terrible has happened. Alternatively, you would use TryParse() for user input knowing that users will definitely not be trusted to always type a number. Not having non-throwing versions of these methods (e.g. Java) means you lose the ability to signal intent and you have to do a lot of pointless catching of exceptions.
- TheSoftwareGuy 7y agoOn the other hand, if you are writing library code or even a library-ish component of application code, having to write the same method a bunch of different times is tedious. Ideally, the language would be designed so that there is one sane way of propagating errors, which can be easily either handled or propogated. I am quite a fan of Rust's solution, with Result<T> types and various macros/methods which can do the commmon stuff for you
- wvenable 7y agoIn this case, it's the difference between what is an is not an error. Only one of those situations is an error. I agree having to write the same method a bunch of times is tedious but that is usually not the case. Most situations have clear intentions. It's much more likely in framework APIs where there are a bunch of non-task-specific operations. I find the whole manual propagation of errors, even with a lot of helpers, to just be tedious boilerplate. Most of the time I literally don't care about the error -- I want to crash my app, log the error, and alert me and the user. I don't need to propagate the potentially limitless number of errors possible in any non-trivial application.
- noisem4ker 7y agoIn Java, you can obtain a throwing version of anything returning an Optional type: Optional<Womp> getWomp() {...} Womp womp = getWomp().orElseThrow();
- hn_acc_2 7y agoActually, I think exceptions are the clearest way to represent errors of any kind (reconcilable or not). > A null value, a plain "error" object, or an error argument In my mind, this just adds a lot more mental overhead while also being less informative than a simple exception. Plus you have to keep track of which method your current library has chosen to represent errors. Note I am biased from using Python for a long time.
- mratsim 7y agoFor me exceptions are for exceptional cases. Any time you have user input, you can expect errors and so should have proper paths to handle file not found, invalid input, ... For those case, exceptions should not be used.
- Seenso 7y ago> A record not being found is a normal thing! It's not a normal thing for code that needs that record that wasn't found. > They literally tell you nothing and there's no way to solve them without catching/rescuing them. A null value, a plain "error" object, or an error argument in a callback would have been sufficient. If I need an exception to be raised for this kind of thing, I'll do it myself. They tell you lots: you asked for something your code path wanted and your request couldn't be satisfied. Also, you've been helpfully kicked onto the alternative execution path to handle that situation. Exceptions are a hell of a lot better than littering your code with null checks or error code checks, especially when you forget one and get a null pointer error or your code wanders away from the root cause and fails later.
- ravenstine 7y ago> It's not a normal thing for code that needs that record that wasn't found. No offense, but I don't know where that idea comes from. Systems are checking for records all the time in ways where the absence of data doesn't necessitate throwing an exception. For instance, a page, user, or piece of media on a website may have existed at one point but was since deleted, but still has a permalink floating around the net. Is it useful, in the case that someone clicks on such a link, to throw an exception when I can instead choose to render different page content when a record wasn't found? I'll never need a stack trace for that. > They tell you lots: you asked for something your code path wanted and your request couldn't be satisfied. Also, you've been helpfully kicked onto the alternative execution path to handle that situation. Why would I want a code path for expected behavior? I agree for cases like a failed connection, where the system is actually broken, but there isn't anything fundamentally broken about data absence. > Also, you've been helpfully kicked onto the alternative execution path to handle that situation. That's not only presumptuous, but if I wanted that to happen, I can do so myself. > Exceptions are a hell of a lot better than littering your code with null checks or error code checks, especially when you forget one and get a null pointer error. Hmmm... const record = store.findRecord(params.id); if (record) { render('show-record'); } else { render('record-not-found'); } or... try { const record = store.findRecord(params.id); render('show-record'); } catch(err) { if (err.name === 'RECORD_NOT_FOUND') { render('record-not-found'); } else { throw err; } } I'll let people decide which one is better. I personally prefer the first one.
- shawnz 7y agoYour example is a good argument for using null or empty list in situations where that is applicable, but it doesn't explain why a custom error object or callback would be better than using an exception, even in reconcilable scenarios. In fact I think using a custom error object would go against your general point of taking advantage of preexisting language features where possible.
- ProstetnicJeltz 7y agoWe tried to use a result type in C# for cases like this. For example, we want to save an object, returning the updated object (on success), or an error on failure. Unfortunately in static languages this leads to an unholy amount of boilerplate: public Result<SomethingGood, SomethingBad> PerformUpdate(int goodThingId, UpdateForm form) { ... return new Result<SomethingGood, SomethingBad>(error); ... return new Result<SomethingGood, SomethingBad>(obj); } The type annotations were hell. Sometimes there were three cases we wanted to consider. We went back to using exceptions, and are keeping a keen eye on the new C# features. So ultimately - I agree with the author that throwing exceptions is fine, when things actually go wrong. It's also not that bad when things kinda went wrong, but sometimes the effort required to fix it isn't worth it.
- lllr_finger 7y agoIt's been awhile since I've been in C#, but in other static languages like Rust and Kotlin with type inference, it would look more like: public Result<SomethingGood, SomethingBad> PerformUpdate(int goodThingId, UpdateForm form) { ... return Err(error); ... return Ok(obj); } which is considerably less boilerplate-y. When there are three cases you want to consider, that's no longer a sum type consisting of just success or failure - that's a different sum type. Maybe even represented as Result<ThreeCases, Error>.
- UK-Al05 7y agoFrankly, that's because C# doesn't support proper discriminated unions in a concise way. F# is a lot better at using result types. You don't even need to specify the return type.
- barrkel 7y agoYou're right; if the only way to test for existence of something is to try and fetch it and check for an error state, then the API is forcing all the costs of its error handling on very regular uses. And if you're dealing with non-determinism, then even a pair of APIs which let you separate out existence-checking from fetches isn't enough; you just introduce a race. I like how .net does it with methods returning bool and an out parameter. Pattern matching on a return value would be fine too. It only makes sense on methods where you reasonably do need to handle the error case. And a variant which triggers an error condition on failure should exist too, for simpler programs which don't need to handle such failures, merely log them.
- lenkite 7y agoI like the new style C++ FileSystem API's where you can either have an error code set or a thrown exception depending on the method overload you choose. I like this a lot since generic utility methods can have use cases where you sometimes want an exception thrown or simply an error code returned. If I am working in Java and a checked exception is thrown, my method usually allows the caller to supply an Function<UnderlyingException, DomainException> so that the caller can specify that a domain exception should be thrown instead of having to try-catch the thing. But I agree with the author of the article - Exceptions are the right design technique when the caller considers an error as a failure as opposed to an unusual but handlable situation. Messing with Result in a deep call-stack is painful.
- burtonator 7y agoThe biggest issue I've seen with exceptions is that people bring up the issue when they are abused. I SORT of agree with you that an 'Record not found' exception is an abuse. This should probably be a null-object like an Optional ALA Java or Scala. They are trying to avoid returning null in this scenario. Unfortunately, if the API is designed poorly you're stuck with it. I can sort of understand why people hate checked exceptions but working without exceptions is just terrible.
- jfengel 7y agoNulls are also called the billion-dollar mistake (and that was decades ago; it's much more than that now). Both nulls and exceptions are ways of trying to make the main line of processing clear, while handling other lines in structured ways. There's no one-size-fits-all solution. In a lot of ways, the best response to "record not found" is that you get the same result as finding one, except with zero answers. That means your main line can process the same way -- unless that doesn't produce the result you need. In which case maybe you an if(null) somewhere, or a special fake record, or an exception.
- aikah 7y ago> Nulls are also called the billion-dollar mistake (and that was decades ago; it's much more than that now). Both nulls and exceptions are ways of trying to make the main line of processing clear, while handling other lines in structured ways. At the same time before NULL, devs used to use "guard values", so NULL is really just a convenience. for instance, just to illustrate what I'm saying: let NULL = {} /* should every lib define its own null value? */ function GetOneRecord(){ let dbResult = queryRecordsFromDB(); if (dbResult.length === 0){ return NULL } return dbResult.records[0]; } Optional types, are better, but one needs functional programming features for it to be really useful. What's people opinion on zero values by the way?
- detaro 7y agoWhat "functional programming features" does one need for useful optional types?
- dahfizz 7y agoThe usefulness of exceptions in the cases you present is to force the developer to handle the cases where things go wrong, such as no records are found. There are more clever ways to do this nowadays with monadic Result types, but the core idea of forcing a developer to handle error conditions is useful in API design.
- mrec 7y agoIsn't that "forcing a developer to handle error conditions" only true of Java-style checked exceptions, though? I'm not aware of any non-JVM languages taking that route, even the very Java-clone-y C#, and thought it was widely believed to have been a mistake.
- _ZeD_ 7y agoStill some people like me think that checked exception are the damn right way to express the possible outcome of a function. Every time I need to work with C# / python I need to play "catch" to keep track of all the possible exceptions that could be thrown at any function call...
- interlocutor 7y agoUnchecked exceptions are a disaster. In C# when you call a method you have no idea what exceptions can be thrown by the called method unless you inspect the called method and all the methods called by it. The called method can be modified at any time and a new exception can be thrown and your code will compile just fine. This is bad because the new exception may be a recoverable condition and instead of recovering your program will crash. This is why exceptions that can be thrown by a method should be considered part of the signature of the method and you should get a compile-time error (as opposed to a runtime crash) if the method throws a new exception that wasn't there when you originally wrote the code.
- mrec 7y ago> In C# when you call a method you have no idea what exceptions can be thrown Well, you have no idea in Java either, given that unchecked exceptions exist. I understand the goal of checked exceptions, I just haven't found much value in them in practice, and given that nobody has copied them I don't think I'm alone. I'm sure it depends what area you're working in, but in my experience it's rare (not unknown, but rare) to be able to do much with e.g. an `IOException` at the call site, and naive approaches like automatic retry can easily be worse than crashing if those retries end up looping excessively and spamming a resource that was already struggling. My personal preference is for non-exception mechanisms (Maybe/Option/Result objects, or C#'s TryXXX methods) to handle recoverable cases, and unchecked exceptions with very high-level catchers to convert the nonrecoverable ones into sensible diagnostics. YMMV, obviously.
- hinkley 7y agoThere are some areas of software design where essentially any strategy 'works' for a year and a half, or two years. If you come into a project you might not notice the friction caused by an arbitrary solution closer to when you are looking to move on than to when you arrived. Testing is big this way, and I've had many 'discussions' with people who could demonstrate no experiences dealing with the consequences of their testing decisions two years on. Either they were gone or had externalized those consequences to others. Getting multiple exceptions from multiple layers of the code only becomes problematic after the depth and breadth of the call tree has passed the point where a single developer can keep it all in their head.
- xeonoex 7y agoI think that's too simple of a case to really make an argument either way. Just return a Some/None or Optional. Null is ok too, but not the best (it's baked in to most languages already though). But at a higher level, if the program expected something, it may be an exception. Let's consider another case. What if you made the request and your session got terminated? That will be an exception too, and I don't think anyone will add that to the result type. I feel like encapsulation means that functions often should throw errors, because the small, one responsibility functions shouldn't have knowledge of control flow. For example, division. Anytime you divide, you might try to divide by 0. That's an exception. The / operator returns a number or throws an exception. Who would use a TryDivide function? What would it solve? The same error might be an exception in one scope, and expected in another. I get that exceptions can be expensive, but they should be rare. Saving time on the pre-check for an exception might save more time than the rare exception. I would never throw an exception strictly for control flow, but avoid exceptions is a bad idea IMO. But the line is very blurry.
- hota_mazi 7y agoIt's a pity they're called exceptions, they should be called errors. Errors can be of two kinds: recoverable and unrecoverable. Recoverable should be handled by checked exceptions and unrecoverable errors should be handled by runtime exceptions.
- dragonwriter 7y ago> For example, if a query is made to an API for a record and that record isn't found, the SDK wrapping that API would raise a "Record not found" exception. What the hell? A record not being found is a normal thing! There are scenarios where it is and scenarios where it is not, it's fairly common for APIs to present both throwing and non-throwing variants (many languages stdlibs, for instance, provide both mechanisms for associative array lookups), and of course in any language with exception handling you can convert either to the other according to the needs of the consuming process at the point where you interface just by wrapping the finction in one which either throws or swallows the appropriate exceptions, depending on which direction you are converting.
- z3t4 7y agoThe slow, stupid, and impractical way is to first ask if any records exist, before asking for the records. Remember that computers are dumb, but they are also fast. Asking for the records without first asking if they exist is an optimization. The records might exist when you ask if they exist, but then they might be gone when you ask for the actual records, now that would be an error.
- naikrovek 7y ago> If I need an exception to be raised for this kind of thing, I'll do it myself. This is the thing. This is the root of the thing. Let developers decide what's right for themselves. There are too many "me-too" developers writing terrible code according to terrible guidelines and terrible "best practices" (lol) and few people are actually thinking about what they're doing. I agree with everything in your comment. Every single thing, and I wish more people understood your (our?) point of view.