Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

>The performance overhead of zero-cost exceptions is not a theoretical issue. I remember stories of Eclipse taking 30 seconds to start up when compiled with GCJ (which used zero-cost exceptions) because it throws thousands of exceptions while starting.

Then maybe the solution is to not have it throw thousands of exceptions while starting?

And have them be, well, exceptional?



APIs have to be designed to allow that in the first place. For instance, the easiest way to check whether a file exists and open if it so is to try to open it and handle the error, because "look before you leap" can be racy. But in exception-based languages, the only way to do that is typically to catch an exception, because the core file I/O APIs throw exceptions on error.


They do throw exceptions, but should they? Is trying to open a file that doesn't exist really an extraordinarily special condition? No, that happens all the time. File systems are known to be messy, untidy places that constantly have problems.

Opening a file should optionally return a file, and if it didn't work return an error value instead. This can be solved with multiple return values (like Go), or an easy to use option type (like Rust). Even C solves this by returning NULL and setting the global errno. But since Java doesn't have either, it uses exceptions for completely mundane cases which then makes their performance much more difficult to tune since they're everywhere.


Sure, but why would there be thousands of files missing for example?

I can understand something like an unmounted drive or a network being down leading to a slow Eclipse launch due to thousands of exceptions.

But those 2 are exceptional circumstances in themselves, and I think it's OK for exceptions to slow down a program in this case.

But would those 0-cost exceptions be much of a burden in a normal run of a program?

I don't see how checking for thousands of missing file references (or anything similar) is representative of what a program does in normal runs.


Consider, for example, checking for robots.txt and favicon.ico. Any search engine or browser is going to generate tons of 404 requests for these resources. If your HTTP library throws exceptions on 404, you're eating an unnecessary cost of stack unwinding on almost every navigation.

IIRC the Eclipse case was similar, checking for optional plugin metadata files or something like that.


An integer parsing function should not throw an exception on failure to parse (see .NET's Int32.TryParse()); and an HTTP library should not throw an exception on 404. These are spurious examples typically inspired by Java's dreadful (especially early) API design.

A REST API library might throw an exception for a 404. But even then it's debatable, because network errors are very common, so they're a poor fit for exceptions. Networks are laggy, so they're also a poor fit for synchronous APIs. Many procedural programming techniques are on shaky ground when they try to paper over the network.


TryParse is a hack around exception performance. In fact, all the TryXXX things usually are. It's purely an unfortunate hardware/performance decision that impacts library design. In F#, I'm happy to write:

  let i = try int s with _ -> -1
OCaml does some similar things, I think. You might use it to attempt to get an item from a dictionary, and return a default if not found. It's easy and elegant.

Saying "exceptions are for exceptional cases" is meaningless wordplay. Call them faults, traps, errjumps, whatever and it breaks down (faults are for faulty cases?). Library design should trump all, and sometimes the better way is to throw in many cases.


No, it's more than that. Exceptions are a useful signal that something has gone wrong with the application. If you can get into that mode of thinking, other benefits flow out of it. You don't get in the habit of catching low down in the stack unless it's a deliberate retry mechanism. You can rely on your debugger breaking on exception meaning something, rather than having to set detailed and careful exception breakpoints. When you use exceptions as a glorified alternative function return value, you risk creating accidentally broad exception firewalls - if you don't catch just the right subset of exceptions, you start swallowing genuine errors.

My advocacy of exceptions for exceptional situations is a cultural one, and the primary aim of the culture is so that you don't catch exceptions. You should almost never catch them. They should almost always bubble up. Only the server handler dispatch or UI event loop should catch, most of the time.

And you can only make that work if the library doesn't throw exceptions for situations where you are likely to want to handle them.

I tend to think of errors as falling into three classifications: (1) incorrect user input, (2) unexpected external condition revealed through a system call, and (3) programming error.

You almost never want to catch (3) - null pointer exceptions, overflow, division by zero, index out of bounds, etc. In some apps, you probably want to terminate. You don't want to catch the exception, you don't want to catch, wrap and throw the exception, because these just obscure the nature of the problem.

You might occasionally want to catch (2) to perform a retry or use an alternative strategy. Depending on how expected the error is, it might make sense to have two different styles of invoking the operation, one that throws, one that doesn't throw. Some code just cares about the happy path and doesn't mind blowing up on failure, other code needs to try harder.

But (1), user error, requires design. It means the user - normally the guy who, however indirectly, is paying for the code - has made a mistake, and that's probably our fault; we didn't do enough communication. We need to work hard to correct it. Exceptions come with technical messages. Technical messages are not user friendly. Throwing an exception would just mean we'd have to catch and translate it. It creates work. It doesn't help. Transporting the message up the stack automatically doesn't help either; context that could communicate the problem to the user is lost. Exceptions simply aren't the right tool. The affordances are wrong, automatic unwind is wrong, and the instincts they inculcate about catching, break everything else.


Yeah, this is interesting. Exceptions come with 2 different moral guidelines that actually contradict each other. The contradiction is not obvious on the surface:

(1) Exceptions can make your contract with your calling code nicer by offering an alternative way of unrolling the call stack. That way, your caller can deal with anything unexpected in a more convenient way.

(2) But the price of being able to do so is expensive. The whole system is designed and optimized for "normal" procedure calls making normal procedure calls and returning normally. Exceptions go against that grain and are very expensive. So only use them for "truly exceptional" situations.

The problem is, if you try to take advantage of #1, you run afoul of # 2.

TryParse vs Parse is a perfect example. The calling contract of Parse is so much cleaner because it takes advantage of # 1. But having an exception occur is too heavy because it runs afoul of # 2.

Problem is, an exception is almost _always_ too heavy. So it is an illusion (delusion) to think you can ever freely enjoy the benefits of # 1. If you need a guideline like # 2 at all, then # 1 is a lie.


So if you aren't using exceptions for all errors, then you need an error handling strategy for those non-exception errors such as parsing failures and 404s. Which brings us back to Rust's strategy: the Result type.


Those languages have choices: you can use exception handling or not, depending on the API context. Exceptions should be used exceptional errors. For example, even in the file exists case, you can have an API to check existence (Java and C# do!), but maybe the file is deleted between that check and the open call...

Now without exceptions, you still have an existence check AND have an error-encoding result on open that is rarely in failure mode. Very annoying. You must combine the two calls even if separating them makes more sense.


Sorry... I want to make sure I have this right. Is your argument that exceptions would a better idea really that the version with sum types doesn't let you get away with the common antipattern of checking for file existence before opening it? Because what you should actually do in that situation (not, I stress, just in Rust, but in any language!) is just try to open the file, which makes the check-and-open operation atomic and results in a single error call. In other words, this is an example of how the Result type guides you towards the correct solution--which is pretty much the strongest reason I can think of for including a feature in a language.


I'm saying, for the file example, you might want to check existence first anyways regardless as part of feedback to the user. You might not even want to open the file, just present it in a list of files that could be open.

I get it that Rust is designed for the command line where that doesn't really occur.


I find this example extremely unconvincing. If you're not talking about an immediate check (for example, you have a list of files in a sidebar), it's quite possible that it hasn't been refreshed for a long while; there's no particular reason to assume that the file is still there. And even if it is, there are many other errors that can occur when you open a file (insufficient permissions, for example). In that situation, I don't see why it's better to fail with an (unchecked) exception, which the developer is going to have to remember to catch and try to tie back to the originating event (which might be quite tricky, given how much I/O occurs in something like an editor), than to require the developer to handle the potential failure immediately (by popping up a dialog box saying the file was deleted, for example).

In any case, what you're talking about is hardly the common case to optimize for. In basically every language I've ever used, the majority of files were opened without any user interaction, in which case Rust's API is absolutely the correct approach.

That is not to say that there aren't times where you have to do a redundant check, but generally speaking they only occur when you have control over the entire system under consideration and statically know (perhaps can even prove) that the error can't happen. In that situation, you should of course unwrap(), as there is no good way to recover from a logic bug in the existing program.


> If you're not talking about an immediate check (for example, you have a list of files in a sidebar), it's quite possible that it hasn't been refreshed for a long while; there's no particular reason to assume that the file is still there.

Exactly. It is probably still there, just someone could have deleted it in the few seconds or so that it took for the user to make an action. UIs are filled with a bunch of invariants that are probably true but not necessarily so. 99% of these programs will just exception out if say that file is deleted between the non-atomic time it takes to do verification and action. And there is nothing considered very wrong with that: you screw with the environment of an application while its running, bad things are expected to happen.

Now, a language like Rust expects that everything is atomic in the normal case. But since users aren't very batchy, that means re-verifying things over and over again. It is not the overhead that is the problem here, the fact that the check is redundant is not the issue. The fact that the programmer is pestered into handling these cases is a huge issue: Java/C# will just exception out and that's the end of it (you don't really want to bother trying to handle that file being deleted in the few seconds it took for the user to make a decision, or if you deal with it, deal with it at a very coarse granularity).

Rust is not designed for writing interactive programs, it is designed for batchy systems code where "files are opened without any user interaction." But let's not discount why existing languages that are designed for writing user facing code make the decisions that they made.


I think we've hit on our point of disagreement:

> 99% of these programs will just exception out if say that file is deleted between the non-atomic time it takes to do verification and action. And there is nothing considered very wrong with that: you screw with the environment of an application while its running, bad things are expected to happen.

I would much rather strive to always do the right thing, if possible, because I do consider that wrong. The pervasiveness of the `Result` type in Rust makes it much easier for me to do so; the cost to you is that you must write `unwrap()` instead of nothing. On the other hand, if unchecked exceptions are the error handling default, I have no recourse in order to discover what possible errors can occur from a function call other than to read the function's source code, while you just save typing `unwrap()`. To me, Rust's solution seems like an eminently reasonable tradeoff.


I get that philosophy, it works wonderfully in batch non-interactive environments. But when almost everything is occurring non-atomically, you wind up throwing unwrap everywhere since nothing is actually guaranteed, the state of the world can change between any operation, even if that is unlikely.

I've specialized in very interactive systems so my world is quite messy. When you run a compiler (and the program being compiled) while the user is editing it, there are lots of transient error conditions to deal with. I just couldn't imagine writing such a system in Rust, error propagation and resolution is a much more global affair that can't tunnel explicitly through function signatures. But then, I understand that Rust wasn't designed for my problems.


Yet, you still end up seeing stories like https://www.coffeepowered.net/2011/06/17/jruby-performance-e...


"should" is a fine word, but there's also the reality that using exceptions in non-exceptional conditions is what they did.


Java is a poor example of good exception practice, and isn't a strong argument against them, IMO. Java went all-out on a particular (flawed) exception model, and pushed it as far as it could - too far, too soon.

I'm a much bigger fan of Delphi's exception model, for example. You would not normally see any exceptions on an app startup, not least because the default setting in the debugger is to break on exception and it would drive the developer up the walls.


If exceptions were instead named traps or faults, or even just errors, what would the prescriptive advice be?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: