11 ms·
It seems all too often we (coders) are encouraged to think of errors as these exceptional things that happen rarely; deserving only of a few cursory preventativ
by electrograv 8y ago
It seems all too often we (coders) are encouraged to think of errors as these exceptional things that happen rarely; deserving only of a few cursory preventative treatments to the code -- or worse, treated as something only to be fixed lazily, as bugs and crashes surface during testing. Indeed -- this philosophy is ingrained into a significant percentage (if not the majority) of the programming languages we use!
On the contrary, I believe we should expect to spend the majority of software engineering time writing and thinking through error-handling code, before even your first test[1].
I'd even go so far as to say: If you're not spending at least half your time on so-called 'error handling', you're probably doing something wrong, like using a language feature (like exceptions) to defer that technical debt to later -- and you'll regret it, if your project matures, I assure you.
This is why I so greatly appreciate languages like Rust and Zig[2] which remove exceptions and unchecked null pointers from the language entirely (with few edge-case exceptions), and provide a powerful and robust type system that allows us to express and handle error conditions naturally, safely, and elegantly.
[1] To be clear, by no means am I downplaying the importance of test code, or even manual testing; rather, I'm arguing that purely "test driven development" is not sufficient to yield extremely robust software of significant sophistication.
[2] These aren't the only examples, but they're among the only that aim to be C++ (Rust) and C (Zig) replacements, that also made the "right" design choices (IMO) in removing both exceptions and unchecked null references.
- IMTDb 8y agoI have always heard that "Exceptions" should be "exceptional". Meaning that most expected errors (eg: getting a non 200 response to an HTTP call, having a file not found when opening one, ...) should not be handled in exceptions, but in regular if...else blocks instead. Exceptions are great for exceptional stuff we really could not have expected (or just don't want to deal with so we want to cleanup nicely), but they tend to be overused for "anything that is not the expected result. On the other hand, deferring non expected path to later using exception like you clearly state sometimes actually is good thing as it speeds up development time significantly. Depending on the project, you may actually really not care, and having that power in your hand in invaluable.
- electrograv 8y agoMy argument: 'Exceptions should be exceptional', because exceptions are really just errors, that happen to be exceptional (rare)! The reason exceptions tend to be overused is because the line between error and exception is blurry -- and it's blurry precisely because an 'exception' is really just a subset of errors. Unfortunately, most languages do not treat exceptions as a subset of an error, but as a completely disjoint/orthogonal thing, and that's the problem! If we transitioned to languages that handle these concepts in a unified way (and ones that don't allow unchecked exceptions), this isn't a problem at all, and we can all happily write much more inherently reliable software.
- mytherin 8y agoI agree with this. Error handling code and exceptions are not mutually exclusive, and both have their benefits and drawbacks. Checking return codes in actually exceptional circumstances makes the code base almost unreadable, and exceptions work amazingly for this. On the other hand, using exceptions for common errors is both inefficient and ugly. As an example of useful exceptions: malloc errors. In C, handling malloc failures is a nightmare. So much so that very few programs actually do it properly. The reason for that is that not only do you need to check the return code of malloc, you need to check the return code of every single function in your entire program that ever calls a malloc anywhere down the line. So while this piece of code might be nice if you see it in one or two places: int rc = fx (); if (rc != 0) { handle_error (); } Properly handling malloc errors means every single function call becomes a four line monstrosity, basically blowing up your code base by factor four and making the code much more difficult to read. Not to mention how insanely error prone it is to have to check the return code of every single function. When doing the wrong thing is so much easier than doing the correct thing, most people will do the wrong thing.
- int_19h 8y agoThis is an API design issue. The proper solution is to provide two versions of malloc or equivalent - one that doesn't have any error code, and simply panics on failure to allocate, and another that provides a way to recover. A typical app would then mostly use the first version, and very occasionally the second when it anticipates that allocation might be so large that it could fail even in a healthy environment (e.g. loading an entire file into memory).
- Mindless2112 8y agoI think Midori might have gotten error handling right, without getting rid of exceptions. It's too bad it only lives on in a series of blog posts. I wish I could summarize the error model here, but I really couldn't do it justice. Read the blog post -- it's very good. http://joeduffyblog.com/2016/02/07/the-error-model/ http://joeduffyblog.com/2016/02/07/the-error-model/
- nostrademons 8y agoThis strategy tends to fail economically. The tech startups that succeed are usually ones that let their customers do things they would not otherwise be able to do. Usually doing something that nobody has done before is hard enough without considering the corner cases; if it follows a typical 90/10 rule, then doing 100% of the job will take 10x as long as the competitor who's only doing the easiest 90%, and your market will have been snapped up by them long before you can release a product. Customers would rather use a product that works 90% of time than do without a product entirely, at least if it delivers functionality they really want but can't get elsewhere (and if it doesn't, your company is dead anyway). Once you've got a commanding lead in the marketplace you can go back and hire a bunch of engineers to finish the remaining 10% and make it actually work reliably. That's why solutions like testing & exceptions (in GCed languages) succeed in the market: they can be bolted on retroactively and incrementally make the product more reliably. It's also why solutions like proof-carrying code and ultra-strong (Haskellish) typing fail outside of markets like medical devices & avionics where the product really needs to work 100% at launch. They force you to think through all cases before the program works at all, when customers would be very happy giving you (or a competitor) money for something 80-90% done. Someday the software market will be completely mature, and we'll know everything that software is good for and exactly what the product should look like and people wouldn't dream of founding new software startups. At that point, there'll be an incentive to go back and rewrite everything with 100% solid and secure methodologies, so that our software has the same reliability that airline travel has now. That point is probably several decades in the future, though, and once it happens programming will not be the potentially extremely lucrative profession it is now.
- electrograv 8y agoI'd agree that it's totally reasonable to 'hack together' a quick prototype with 'duct-tape and cardboard' solutions -- not just for startups, but even in full-scale engineering projects as the first pass, assuming you intend to throw it all away and rewrite once your proof-of-concept does its job. The problem is that these hacky unstable unreliable solutions sometimes never get thrown out, and sometimes even end up more reliable (via the testing and incremental improvement methods you mention) than a complete rewrite would be -- not only because writing reliable software is hard and takes time (beware the sunken costs fallacy here!), but because sometimes even the bugs become relied upon by other libraries/applications (in which case you have painted yourself into a REALLY bad corner). It's a balance, of course. You can't always have engineering perfection top-to-bottom (though I would argue that for platform code, it has to be pretty close, depending on how many people depend on your platform); if you shoot too high, you may never get anything done. But if you shoot too low, you may never be able to stop drowning from bugs, crashes, instability, and general customer unhappiness no matter how many problem-solver contractors you may try to hire to fix your dumpster fire of code. So again: Yes, it's a balance. But I tend to think our industry needs movement in the "more reliability" direction, not vice versa.
- sk5t 8y ago> If you're not spending at least half your time on so-called 'error handling', you're probably doing something wrong, like using a language feature (like exceptions) to defer that technical debt to later -- and you'll regret it, if your project matures, I assure you. Clarification please--are you suggesting the programmer can anticipate all reasonably likely error conditions and implement handlers for each? I lean towards treating recoverable errors as uncommon, preferring a log->retry->give up strategy in most cases. The biggest sin is often in obfuscating the source error or prescribing a bogus resolution. Exceptions, while not perfect, remain a pretty good way to convey what was going on when things went sour.
- electrograv 8y ago> Clarification please--are you suggesting the programmer can anticipate all reasonably likely error conditions and implement handlers for each? Absolutely, insofar as it is possible for a programmer to write their application in one of the several existing programming languages that can guarantee at compile time that there is no undefined behavior / exceptions / crashes / memory corruption. This concept is sometimes referred to as a "total function": a function that is defined for all possible input values, and in a language for which there is no way to invoke that function on an input not in its statically-declared domain. Now, I'm not saying this is possible for all programs, particularly for certain kinds of I/O, but with a reasonable amount of error handling code and robust interfaces between that I/O device and your code, it's usually possible to get pretty close to perfection there too. I'm also not saying it's easy. But I think this is precisely the kind of "hard" the software engineering industry needs more of right now; and languages like Rust and Zig and even Spark (Ada verifier) are a wonderfully refreshing move in that direction. Note: Not all of these languages I mention are 100% perfectionist either! What's important is they move significantly closer to perfection, and away from this 'cowboy coding' mentality of not really caring about errors/exceptions until they bite you.
- sk5t 8y agoReally puzzled that you have conflated exceptions ("'x' went wrong, here's the stack") with undefined behavior, crashes and memory corruption. It seems as though you're describing how the world ought to be, and maybe could be, for the writing of pure functions, procedures with provably no side effects, and the like. Well, OK. However, imagine that you're writing a few bytes to disk. Maybe the device runs out of space halfway through the write, the filesystem was mounted readonly, or the filesystem doesn't like the filename you asked for due to length or case-insensitive overlap with an existing name, or the controller driver glitches out, or permissions are wrong, etc., etc. You cannot anticipate all of these and implement recovery, aside from informing the caller what went wrong, and giving an opportunity to somehow correct the problem elsewhere and retry. Well now you're in the business of not really recovering from the fault, but instead doing something the same or nearly the same as raising an exception and printing the stacktrace. TBH I have misgivings about even writing up that last paragraph because the alternatives really strain credibility.
- derefr 8y agoErrors and exceptions are really fairly different things, and I personally appreciate languages like Erlang that have both. "Errors" are an instance of a (perhaps-implicit) returned sum-type of a given function. Calling a function that returns a sum-type, and then not having a branch for each instantiation of that sum-type, is almost always a logic error in your code, no matter what the sum-type is. A pretty good generic solution to "error-handling" is the Either monad, as seen in its literal form in Haskell, in Javascript as a convention for async callbacks, and in Erlang as the conventional sum-type of {ok, Val} | {error, Desc}. The Either monad is nice because you can't really "lower" monadic code by ignoring the monad and just "getting at" the result; instead, you have to "lift" your code into the realm of the monad, which requires you to specify how you'll handle each of its potential cases. (Even if your specification is simply "I expect this; if it isn't this, crash"; or "I expect this; early-return anything else to my own caller.") "Exceptions", on the other hand, are things that by default no component of your system knows how to handle, and which usually—other than some infrastructure-level components like HTTP error handlers and "telemetry" service libraries—code just allows to bubble up until it hits the runtime and becomes a process abort. These include "runtime errors"—for example, failures of memory allocation; and "programmer logic errors"—for example, dividing by zero. In either of these cases, the "correct" thing to do is "nothing at all", because the system itself has no idea how to handle these at runtime. The only time when these problems could have been handled was at development time, by writing more or different logic such that these exceptional circumstances would never arise in the first place. Now that you're in such a situation, though, you pretty much just want to note down what happened (so the log can be sent to a developer, who can use it to understand what the novel exceptional situation was, and then plan a change that will prevent the exceptional situation in the future) and then die quickly, before your now-invalid runtime state can do harm to things like durable stored state. Or, to put that another way: • an error says "I failed to do that." • an exception says "I have noticed, while trying to do that, that no valid system-state can result, even one of failure."
- electrograv 8y agoI don't think this alleged distinction between exceptions and errors is as clear-cut as you imply. I think this distinction is purely a convenience; a line we draw to make our lives easier because we don't really want to accept that extremely rare error cases do exist and should be reasoned about despite their rarity. For example, you listed as some examples of exceptions that are not error: Divide by zero, failures of memory allocation. Let's say you write a calculator GUI and I try to divide by zero in it. Exception or error? If I applied your advise, I would have to deem this an exception, and just "just note down what happened" or "die quickly" (your words)! That is quite obviously wrong, in this example. The correct answer would be to feed some kind of error code to the calculator's output data path, which would eventually be displayed on the screen. Sure, I'm sure you'll come back and say now "well it depends then, and whether you consider it an exception or an error depends on the application". If you say that, you are ceding my point, because that is precisely what an 'error' (not an exception) is: a condition that may in some cases be fatal to the application, and in some cases be a part of ordinary runtime behavior.
- barrkel 8y agoRust's Result type is isomorphic with checked exceptions, FWIW, just a bit more explicit in its use.
- int_19h 8y agoIt's also more flexible and composable, since checked exceptions aren't quite a proper part of the type system. Consider the case of a higher-order function that wants to derive its exception specification from the function it wraps.
- barrkel 8y agoEh. Very easy to model as a second return type on the function type. If your function type is polymorphic, it's just another type argument.
- int_19h 8y agoYou also need union types. But yes, you can make it work (the original lambda proposal for Java tried to do that). Of course, once you push it to the point where it does, then it is practically indistinguishable from having a single return value that is a discriminated union.
- barrkel 8y agoI'd even go so far as to say: If you're not spending at least half your time on so-called 'error handling', you're probably doing something wrong, like using a language feature (like exceptions) to defer that technical debt to later -- and you'll regret it, if your project matures, I assure you. If you spend more than half your time in error handling, you have an architectural design issue with your code. If you need to be that vigilant then it's too easy for a junior hire to make a mistake that makes your code unreliable. Design out pitfalls. Reduce the amount of code in your system where you need that level of vigilance, and you'll no longer need to spend 50+% of your time on error handling.
- electrograv 8y agoDesigning out pitfalls is precisely what I’m talking about and arguing for — and that takes time (in an application of any sophistication), either because you’re using a language (like Rust) whose compiler nitpicks your code in ways that 99% other languages would just accept without complaint, or because you’re being equivalently paranoid in your design of every single core data type, interface, platform service, etc. There may also be some disconnect here in the type of code we’re talking about. I’m talking about work on code that millions of people are relying on to be 100% rock solid. I don’t care if you’re junior or senior: when writing code at this standard of quality, caution and rigor are always part of the process (both by the author, and the review and testing process). If you can write absolutely rock solid, 99.9999% bug-free code that handles every possible error case gracefully, while spending more than 50% of your total coding time spent typing new code (which apparently has no error handling) literally as fast as you can type, well... WOW!! Consider me impressed; It seems we all have a lot to learn from you. If so, I genuinely would love to learn more of this seemingly-magical process where you can write perfect code at maximum speed, while also not having to think about edge cases or other errors. Anyways, back to reality: The fact that so much of this caution associated with writing bug-free code is loaded onto human judgement right now is exactly why I’m such a strong advocate for languages like Rust and Zig that aim to move much of this cognitive burden into the compiler. For example, let’s talk about designing out pitfalls: say I create an immutable data structure in C++ with a really efficient implementation (zero-cost copies, automatic memory sharing) that is virtually foolproof when accessed from multiple threads, used in computations, etc. No matter how foolproof I make this C++ class, I still can’t stop your “junior dev” from stomping over the end of another array into my class data, or using dangling pointers, etc. etc. etc. We can enforce “safe” classes wherever possible, but that also runs up against the wall of reality when interoperating with other C++ code that has a different idea of what constitutes that “ideal C++ subset”.
- otabdeveloper2 8y agoThere's no such thing as a language with no exceptions. There are only languages where exceptions need to be hand-rolled in an ad-hoc fashion. This ad-hoc approach does not work. It gives you tiny benefit of not needing to learn a language feature. Meanwhile, ad-hoc exceptions mean that you lose all sense of modularity in your program. Real programs are composed of dozens of modules at various levels of abstraction, communicating together over multiple processes and threads. Ad-hoc exceptions in a real program in practice means 'just crash the whole thing, yolo'. That does not work for serious programs.
- speedplane 8y ago> There are only languages where exceptions need to be hand-rolled in an ad-hoc fashion. This ad-hoc approach does not work. C has no exceptions. It seems to be quite successful.
- jcelerier 8y agoWell no, every time you have a chain of functions in your call stack that all do if(f(blah) != E_OK) { return E_WHATEVER; } (which is veeeery common in C) you are reimplementing exceptions by hand (and with less performance in the no-error case since you still pay for the branches, while a C++ code with exceptions would just be a sequence of function calls without branches in that case)
- speedplane 8y agoI wouldn't disagree with you. The OP said that programming languages w/o exceptions aren't successful. C doesn't use exceptions, it has it's own way of dealing with similar issues, and yet it's quite successful.
- didibus 8y agoI can follow your premise that not enough time and effort ia out towards making sure our programs are correct and have low defect. I'm not sure I follow your jump to claim Rust and Zig solves that problem. But I guess time will tell, when there will be as program running in those as there are C/C++ programs, we'll see if things are any better or not.
- raverbashing 8y agoMost error handling has only one possible way of handling: your activity fails to complete. That's why exceptions make sense. I don't care if I ran out of disk space, a file is corrupted or what else, the result is the same.
- dkarl 8y agoI think exceptions are kind of cursed by their name. To treat exceptions correctly you have to constantly keep in mind that they are misnamed. And you have to deal with type systems that do not treat them as precisely as they treat return values. And on top of that you have to coexist with people who impose all kinds of semantic limitations on their mental model of exceptions, such as that they have to be "exceptional." Exceptions have to be treated as happening all the time, and in strongly typed languages (such as Scala, my hammer) you have to keep in mind that the type system has this huge loophole built in that is very important and that the type system gives you very little help with. (The most obvious example that utterly confounds semantic limitations on exceptions is opening a file. Programmers accustomed to working on a certain kind of system find it quite natural to regard a missing file as "exceptional", even fatal — for them a missing file means the install or even the very build is corrupted. Programmers who work on a different kind of software may regard missing files as completely routine, maybe a commonplace result of user error. If these two groups of programmers believe exceptions are "exceptional" they will never agree on the contract for opening a file. Another example is deciding whether something that happens 0.0001% of the time is exceptional. Some programmers will regard that as exceptional while others will consider it utterly routine and regard it as a failure of discipline to believe otherwise.) (The logical consequence of insisting on "exceptionality" is that you need two sets of basic libraries for everything and may need to mass refactor your code from one to the other as your use cases change. This is a needless imposition that offers no recompense for its offensiveness to good taste and good sense.) The great merit of exceptions is that they remove boilerplate and make it trivial (nothing is more trivial and readable than no code) to express "I am not handling this, someone else must" which makes it quite easy to e.g. signal "400 Bad Request" from deep within some code that parses a request entity. Personally, I think that for now it is best to prefer exceptions for writing lots of useful code quickly and concisely and to prefer strongly-typed, expressively-typed return values for achieving a higher level of care and reliability. But I look forward to a better linguistic solution that combines the virtues of both these approaches — and I have to admit that in my ignorance of many important languages I may be overlooking an already existing solution. I am reminded that in the early 2000s I would have relegated all static typing to the "verbose boring highest reliability required" category and then type inference and other ergonomic advances converted me to statically typed languages such as Scala as the best way to pump out lots of valuable working code. I'm looking forward to a linguistic solution to this problem.
- mypalmike 8y agoUsing exceptions isn't "deferring technical debt". Whether you use exceptions or not, you have to do all these things: detect errors, unwind the stack to a place where the error can be dealt with, and clean up resources during the stack unwind. Exceptions are a control flow tool that simplify these things, nothing more or less.
- hardwaresofton 8y agoExceptions-as-alternate-control-flow is a paradigm that shouldn't have become mainstream in the first place. Using Exceptions (in effect forcing the addition of a new control flow path) unless where absolutely necessary has hurt the field, it's just like null. I much prefer the errors as values, and I think people are coming around to this point of view more and more recently, as languages are being retrofitted with type systems that are good enough to cope. When I first saw Optional in Java (long before I'd ever done any meaningful work with languages like Haskell or Rust), I thought it was weird how it seemed to seep everywhere and be a good idea to use everywhere. This seemed off/weird to me at the time, but now I recognize it as the Blub paradox[0] (I'm not a pg worshipper but this is one of the most insightful things I've read of his IMO). The way I was thinking was wrong, at least from an overly pedantic sort of view -- failure is everywhere in Java due to nullable types, people have just been conditioned to pretend it isn't. Nowadays I don't choose languages for software I write with nullable types not checked by some compiler -- so using Typescript when I do JS, or using Haskell or Rust. [0]: http://wiki.c2.com/?BlubParadox http://wiki.c2.com/?BlubParadox
- coldtea 8y ago>I'd even go so far as to say: If you're not spending at least half your time on so-called 'error handling', you're probably doing something wrong, like using a language feature (like exceptions) to defer that technical debt to later -- and you'll regret it, if your project matures, I assure you. That is a big if. Most projects don't mature that much. And for those that do, regretting things after the project has matured is a "nice problem to have". E.g. even if Zuckerberg has regretted some bad early FB design (let's say regarding error handling), he's is still a billionaire. Second, you can handle or ignore errors with exceptions just as well as with any other mechanism (multiple return values, optionals, etc).
- HeyLaughingBoy 8y agoAs someone who has spent years writing motion control code, I agree wholeheartedly. Thinking of code I've written to move an object from one physical position to another and in pretty much every case, the error handling and recovery paths are the bulk of the code. Errors in motion systems are not rare events. Things get sticky, wires break, sensors get clogged with dirt, parts wear out and break off and throughout it all, this thing has to keep moving from A to B reliably or at least clearly indicate when it's failed unrecoverably before something worse happens.
- billforsternz 8y agoBack in the day I used to work on low level telecommunications code. Often there the happy path was treated as (almost) an afterthought and handling all the error paths was the focus of the work. It was possible to work that way using C or in the various assembly languages we used.