6 ms·
The Throw Keyword Was a Mistake
- al2o3cr 7y agoonly the language runtime and operating system would be allowed to raise exceptions So you still need all the exception machinery, but "normal" code can't use it? Seems like a terrible situation. Also, who's in charge of deciding what's "language runtime" and what isn't?
- Jare 7y agoIt's not really about who can use it, but rather what it is used for - language runtime can't use it for anything they want. So if a systems-level feature of the language is supported in part by runtime code, that's a place where this may happen, e.g. the Go Runtime throwing a stack overflow. Of course the counterpoint is that C++ and to some extent Java are designed for allowing user code to create system-level facilities, so we're back to the land of "please don't misuse exceptions."
- __s 7y agoThis is how it's done in Rust. panic macro unwinds stack for tracebacks, there's std::panic::catch_unwind, but panic can be compiled to be aborts to do away with stack unwinding, error handling is meant to be done with Result<T, E> type, whereas panic is meant for unrecoverable errors
- riwsky 7y agoThis is not an apples-to-apples comparison. In Rust, app code can panic, even though it’s not idiomatic. That does not meet the standard prescribed by the quote from the article.
- notacoward 7y ago> who's in charge of deciding what's "language runtime" and what isn't? Maybe the people who define the language? Who implement and package its standard compilers/interpreters? This is not confusing outside the world of empty rhetoric.
- altitudinous 7y agothrow is being used as a replacement for goto.
- jnwatson 7y agoAll control flow are jumps underneath it all. The only important question about a particular control flow syntax element is: does it allow a human to reason about a particular code scenario more easily. Most of the time, I think exceptions help more than hurt. Like many things in language design, it is a trade off, in this case of concision vs simplicity.
- fxtentacle 7y agoI've always thought of Java as being extra verbose, in part because there's try catch blocks everywhere.
- mjevans 7y agoI've thought of it as being extra tedious.
- tsimionescu 7y agoWherever there is a try/catch block in Java, you would have at least a few if err return/goto cleanup in C/Go/other error-code languages. This is especially true when you have a function which performs multiple operations that return the same kind of error (e.g. multiple HTTP calls, or multiple file-based operations). In general, in Java you need a single try/catch block for all of these (which may well be in another function), but you need an if err return for each one in C. Rust has a more interesting approach, where the sue of Macros gets rid of the verbose if-based pattern, but the code also avoids non-local return. I haven't written Rust yet, but it seems like a very interesting approach.
- erik_seaberg 7y agoA lot of try blocks are only there because stdlib declared a lot of interface methods that forbid checked exceptions, requiring everyone to smuggle them out. This nonsense just goes away in Scala and Kotlin.
- kova12 7y agoI disagree. Throw keyword is the greatest invention, and makes programming so much better. Error codes are the evil. Exceptions allow to separate business logic from error handing in a most effective manner. Author likely have not ever worked on large codebases where the error handling was messed up beyond any repair due to having to pass error codes every friggin where
- jwilliams 7y agoThe opposite to exceptions isn't (necessarily) error codes. Golang errors are a recent example.
- tomp 7y agoHow does that work? I haven’t seen the recent changes so I can’t say for sure, but AFAIK Goland and Rust error handling mostly amounts to line noise trying to recreate the control flow of exceptions (i.e. bubbling any errors up the call stack) (although Rust tries to make it more palatable using macros).
- wizzwizz4 7y agoThen don't try to recreate the control flow of exceptions. Handle things ASAP; only propagate an `Err` if the function returning a `Result<_>` logically has an error condition. Don't be afraid to panic, unless user data is on the line, in which case register an unwind handler and tiny backup facility if you don't trust your programming ability (hint: nobody should trust their programming ability that much unless they've rigorously (mathematically) proven their code's soundness). It's a different way of programming, but a more explicit one, and I don't think implicit Bad Thing™ should be easy in a language. (By the way, Rust has a single-character operator to emulate exception pass-through.)
- tomp 7y ago> Don't be afraid to panic, unless user data is on the line, in which case register an unwind handler Sounds like exceptions.
- fxtentacle 7y agoI strongly believe that the Go approach is the correct way to go. Every function that can fail should return an error code and then the programmer invoking said function is expected to handle the error code. It also used to be that way back in the old C / C++ days before SEH made it messy like Java. And the beauty of using statically typed languages with error code return values is that one can use automated tools to find all the places where error codes are being ignored. So then random error conditions sprinkled throughout business logic go back from "this is how Java is done" to "this is a code smell".
- _pmf_ 7y agoThe old C way was returning an error indicator and providing the error code in the global variable errno(), which has ... issues.
- viuphiet 7y agoA few more steps and you'd have reinvented functional programming.
- wruza 7y ago>Every function that can fail should return an error code Every function that can fail, that can take invalid arguments, or can be called at invalid times. Which widens the set to basically every function. Old APIs had a convention to always return a status directly and results in out-params, except for pure mathematical operations (in which errors were mostly ignored, e.g. in a computer, a+b may fail as much as fopen(), but who cares). Observing this fact, one can free themselves from: if ((status=operation(args, &vague_error_descriptor)) return status; and handle the specific error event where it seems reasonable in the way that seems reasonable. Unwinding call stack by hand doesn’t automagically handle errors. At main() or at the other high-level call site you still have no idea what to do with an unexpected error except logging/presenting it and exiting (or continuing in hope invariants were not broken). >one can use automated tools to find all the places where error codes are being ignored The same is true for exceptions - every milestone like db.open(), file.save() and process.spawn() should be try-catched. And in both cases you don’t know which errors “codes” are still unhandled.
- scarejunba 7y agoWe've moved past it now with having errors just be part of the type and signature so you can use language tools to deal with them like any other type instead of having special syntax. I like that. It allows you to express certain concepts more easily. For instance, "in case of failure, use blank" looks nicer in Rust than in Java, and so you're more likely to use it. Java is also sort of moving this way because checked-exception methods don't play well with Streams syntactically.
- mrkeen 7y ago> The Year Everything Crashed Which year? > If you were using computers in the years that followed you remember the time well even if you didn’t know the reason; a lot of software that had been stable and solid before started crashing and misbehaving. I think I missed this.
- arpa 7y agoWell lp0 would catch fire, but it wasn't as bad as using throw!
- dkulchenko 7y agoI love the way Elixir/Erlang handles this, personally. If the error is expected/should be handled, just add the error to your pattern match and handle it right there (or bubble it up, if appropriate). Truly exceptional situations where something "should never happen" won't be pattern matched against, BEAM will crash the process, and your supervisor will get you back to a good state. Feels so much cleaner than either exceptions everywhere or easy-to-ignore error values.
- fuzzy2 7y agoDidn’t Java require all exceptions be declared in the method signature? (Haven’t used it in years.) I think that’s the best solution. You declare what error conditions could arise, but not using magic numbers. The caller can then decide whether to handle them or pass them to its caller etc. I agree that unexpected exceptions (like it’s possible in C# and possibly others, you can just throw whatever whenever) are far from optimal.
- ramblerman 7y agoInitially yes, but this is somewhat unpopular these days as it means you have to daisy chain a new exception all the way up the stack if you can't handle it locally. Most have switched to runtime exceptions, which are more flexible in that regard.
- riwsky 7y agoNo. Only a certain subset, known as “checked exceptions” (as opposed to “runtime” exceptions), need to be in the signature. In what may come as a surprise, checked exceptions are actually frowned upon in modern java (e.g. new stdlib apis tend to avoid them). They do not compose very well with the rest of the type system (e.g. you cannot write a ”map” function that is polymorphic over checked exception types)—or at the very least, do not compose as well as some other patterns, like Optionals
- fuzzy2 7y agoHm okay. If it’s not “compatible” with streams, higher-order functions and whatnot I can see why it’s not popular. A pity, really.
- mrkeen 7y agoChecked exceptions sound good on paper. In practice I think I re-threw every checked exception as unchecked. In retrospect I feel like 'handling' exceptions is a myth. If you knew how to handle them, they wouldn't be exceptional.
- tsimionescu 7y ago
- lwhi 7y agoWhen 'pair programming' was chucked in as an example of one of the worst things that happened to programming I lost interest. Back up your assertions with facts. If you don't have facts use good truthful examples. Too much opinion and conjecture isn't useful.
- tsimionescu 7y agoThis really reads like an article by someone who likes to think they know better than everyone else, while liking things that the industry has tried and moved away from. I would much prefer a similar sounding article from someone who champions some weird technology, like people APL people who advocate for extreme conciseness of code, or Forth people who advocate for extreme integration of solutions. I don't think they're right, but at least they would be novel and would advocate for something we haven't moved away from as an industry for a reason.
- notacoward 7y agoRejecting the entire main argument because of a single aside - a matter of style more than substance - is effectively argumentum ad hominem. Yes, the author comes across as a grumpy old "get off my lawn" type (like me in fact). No, that doesn't make any of their arguments or anecdotes about throw invalid. Try addressing the issue, not the person.
- lwhi 7y agoWhile you may be able to sympathise with the author's style of writing, I think my point still stands.
- notacoward 7y agoYour point seems to be that it's OK to reject an entire argument because of one irrelevant aside. I guess that "stands" as a personal belief, but it does not stand as an argument or refutation of one.
- lilyball 7y agoThe keyword, and exceptions, are orthogonal concepts that this post is conflating. The real issue this post has is with exceptions (and in particular user-defined exceptions, though I'm not really sure why this matters because you can always (ab)use system exceptions if you have a way to reliably trigger them). You can still use the `throw` keyword with other error-handling models, such as with Swift's Errors.
- DictumMortuum 7y agoI worked at a telecom company. They had a program written in java for generating the initial customer configuration. Among other stuff it used exceptions as control flow and it made me make extraordinarily high estimations for seemingly simple changes. Anyway, I don't think I agree with the article - it's again the matter of the knife and how you use it.
- Gibbon1 7y agoI feel like with errors we're always in a state of sin because the way errors create a combinational explosion of code paths. The only thing I have to say is I have a distaste for throwing exceptions across API boundaries.
- notacoward 7y ago> it's again the matter of the knife and how you use it. The author makes this point repeatedly: exceptions would be great if all programmers were above average, but (of course) they're not. In practice the knife is juggled and thrown (heh) and generally mishandled so much that the floor is covered in blood. This gets us into pg's whole "blub" design-for-geniuses vs. design-for-idiots discussion, with is basically dueling strawmen, so I'll bow out there.
- foreigner 7y agoCould some sort of hybrid approach be an improvement? What if we required every function to explicitly declare the types of exceptions it might throw? The compiler could enforce it and make it transitive to higher level functions.
- tsimionescu 7y agoExcept for the transitive part, this is what Java does. But it's doesn't address the complaints of the article, which is bothered by the non - local control flow and the large cost of the exception machinery in C++, and by the poor interaction between manual memory management and exceptions. Note: I think the article is vastly exaggerated, and completely wrong, at least when looking at managed memory languages.
- ben509 7y agoThat was the basic premise of checked exceptions in Java. It's not really related to the type system or anything. The root of the problem is Java compiles each class as an independent module, so any method has to deal with all its exceptions, even if only naming them, or it can't compile. Thus checked exceptions meant you either got huge signatures, or you wound up wrapping them all into other exceptions, which then made them much harder to inspect. And unchecked exceptions made the problem worse. Java uses single-inheritance and put unchecked exceptions on their own branch. This meant you wound up with, for example, IOException and UncheckedIOException with no close common ancestor, so you had to wrap one in the other. So now, in addition to complicated signatures, you are doing more wrapping. I think what might work would be to ditch the checked / unchecked idea, mostly. (You still want to treat errors like division by zero or invalid array indexing as unchecked.) What you distinguish instead is whether methods are "safe" or "unsafe". An unsafe method can still catch some exceptions, it just doesn't promise to catch all of them. The safe method must catch everything. For example, your main method and Thread.run would have to be safe.
- bluestreak 7y agoThrow in Java is very inexpensive. Expensive part is creating new instance of exception, which figures out stack trace and creates bunch of immutable strings. There is a technique of throwing existing static instance of exception, which generates no garbage and two orders of magnitude faster than traditional java approach: https://github.com/questdb/questdb/blob/7560895ee400c82b0e5e39440eaf6c55bbbb7d7d/core/src/main/java/io/questdb/cutlass/text/types/InputFormatConfiguration.java#L207 https://github.com/questdb/questdb/blob/7560895ee400c82b0e5e... The only downside is that stack trace is effectively defunct.
- javajosh 7y agoI agree that `throw` is easy to misuse; but I think this is a symptom of something else, namely in architectures where input even handler code is spread all around or, worse yet, dynamic. This is really common in many languages, and perhaps my greatest pet peeve. However, throw is a wonderful facility if your architecture has one defined entry point for inputs of all kinds. It means your outermost loop can have a try block, and literally everything else can feel free to throw in the safety and comfort of knowing the entire program won't fall down.
- loopz 7y agogoes on to explain misuse.. jk sort of. What when allocation fails or disk full etc?
- bvrmn 7y ago> What when allocation fails or disk full etc? It is a common misconception that such errors can be easily handled. Most of software is too complex to be able to do something reliable with hard OS issues. Stack unwiding with exceptions is the most quick way to disaster with ENOMEM as root cause.
- bvrmn 7y agoError propagation is must have for any project. You always need to handle some errors in parent context because local knowledge can be not enough. And you can choose between manual passing of error codes (c, go, mundane and boiler-platy), algebraic errors (rust, zig, optional sugar can make it convenient and look like SEH) and SEH. Yes it is additional condition flow, but you have to introduce it in some way.
- classified 7y agoPublishing on hackernoon is a mistake. The site sucks and also disables the Reader View that would make it tolerable.
- bvrmn 7y agoNo, article corresponds to unicorn-rainbow-coin-rebel style of most hackernoon content very well.
- kazinator 7y ago> Structured Exception Handling (SEH) was born. This, including the abbreviation, is the name of a feature of the Windows API. The author is confused between exception handling in programming languages, and exception handling in the MS Windows operating system. https://docs.microsoft.com/en-us/windows/win32/debug/about-structured-exception-handling https://docs.microsoft.com/en-us/windows/win32/debug/about-s... https://en.wikipedia.org/wiki/Microsoft-specific_exception_handling_mechanisms#SEH https://en.wikipedia.org/wiki/Microsoft-specific_exception_h... Note that in Microsoft C++, this uses special __try, __except and __finally keywords, which use underscores because they are extensions, separate from the ISO C++ try and catch. SEH can catch events like access violations. It's more like Common Lisp exception handling in that you can catch errors without any stack unwinding taking place, so that fixing up a fault and re-starting an instruction is possible (like catching a SIGSEGV or SIGBUS on POSIX).
- playpause 7y agoCan someone point me to a code example (preferably TypeScript or JS) to clarify what the author proposes as an alternative to ‘throw’? Maybe it’s Stockholm Syndrome, but I’m struggling to picture how returning an error code (and checking it with an if-statement after every function call) would make code easier for a human to analyse. Seems like it would just make it a lot noisier and less expressive. But I’m intrigued.
- ensiferum 7y agoThe problem comes when programmers muddle and get confused about different possible "failure/error" conditions in a program. A) Bugs, i.e. errors made by the programmers of the software B) Expected logical conditions that only appear as "errors" to the users C) Unexpected failures such as resource allocation failures Let's have a look at these one by one. Case A, i.e. bugs written by programmer, i.e. the program is about to violate it's own constraints/logic/pre/postconditions/invariants. Examples are many, but for example off by one, accessing nullptr/null object, array out of bounds etc. Really the best way to deal with these is to abort/panic/dump core with a loud bang and produce a core dump and a stack trace to simplify debugging. Case B, i.e. conditions that the application should expect to encounter and be prepared to deal with. For example your TCP socket connection times out, DNS query fails, HTTP query response returns 404. In any such case when you need to write some logic such as "retry to open the sockect connection" or "tell the user that the resource was not foudn" or try to resolve DNS query again later the best method is to use some kind of error enum/number/code kind of system. And to recap the "errors" in this category are not errors for the software but only errors for the user. I.e. for the software these are just logical conditions. Finally Case C), errors that are neither A or B. Typically these are just some very unexpected conditions such as resource allocation failures. Your program failed to allocate a socket or pipe or a mutex or whatever. Should not happen but might happen once in a blue moon under heavy system load or unexpected conditions. These are the things you don't want to conflate with your logical "error condition" path from case B because't that won't scale and propagation of errors up the stack becomes very tedious. These are best suited for exceptions. Edit: Note that this isn't a static state but what is considered a bug or a logical error can change across modules depending on the software project organization. A module written by another team migth consider illegal arguments as "Logical error conditionds" and return error codes, whileas a similar module written and used within a single team might just consider this a bug and dump core.
- notacoward 7y agoAgreed. Exceptions should be, well, exceptional. Not normal parts of control flow, or commonly occurring kinds of errors. Assertion failures, or other things that would otherwise send a signal and terminate the process - seg fault, divide by zero, etc. Maybe something like a socket being closed unexpectedly. That's kind of the boundary where it's worth debating. Anything more common/expected than that should be represented as an error code and not completely redirect the program's control flow.
- ch_123 7y ago> Dealing with them in a more elegant way than inelegantly crashing goes all the way back to 1962 in a near-forgotten language called LISP. Even as someone who isn't a Lisp programmer, I find the idea that Lisp is a "near forgotten language" to be a bit of a stretch.