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

Regarding maintainability ("hard-to-follow"), I’ve become a big fan of Java’s checked exceptions (and also their causal chaining and adding of "suppressed" exceptions, which would be nice to have for C++ destructors). I effectively see them as a sum type together with the return type, just using different syntax. It’s an important reason why I stick to the language, because no other language has that kind of statically-typed exceptions.

As the article explains, the problems in C++ are more an ABI issue than a programming language issue (except for the by-reference vs. by-value semantics). You could implement exceptions internally by variant-like return values, for example, similar to how error passing is done in Rust, while still having it look like exceptions on the language level. It would be fun for future languages and runtimes to more easily be able to switch the underlying mechanism, or possibly to be able to use different implementation mechanisms for different parts of a program as needed.



Java's checked exceptions are generally regarded as a mistake. There's a reason no other languages has them, and newer JVM languages (Groovy, Clojure, Scala, Kotlin) treat all exceptions as runtime. Anders Hejlsberg (creator of Delphi, C# and Typescript) also has an excellent article on their problems [1]. In modern Java I see nearly only runtime exceptions used, especially because that's necessary for most Java 8+ lambda API's.

Especially when used as an error ADT they're awful because they mix everything from "you have to handle this 90% of the time" to "the universe physics just changed, sorry" into one construct. Much better to use something like Vavr's Try and explicitly propagate the error as a value.

[1] https://www.artima.com/articles/the-trouble-with-checked-exc...


Most of the software I write is designed to be fault-tolerant, and checked exceptions are fantastic way of detecting potential faults. The problem is that checking is baked into the exception definition instead of its usage.

If I declare "throws NullPointerException", then this should mean I want it to be a checked exception. This should force the caller to catch the exception, or declare throwing it, or to simply throw it out again without declaring it. This would effectively convert the checked exception into an unchecked exception.

Converting a checked exception to an unchecked exception is possible in Java, and I do it whenever it makes the most sense instead of wrapping the exception, which is just a mess. Unfortunately, there's no way to make an unchecked exception behave as if it was a checked exception. It's easier to reduce fault tolerant checks then to add more in.

Some might argue that converting an exception to be unchecked is a bad idea, but this sort of thing is done all the time in JVM languages that are designed to interoperate with Java. If I call a Scala library from Java, and that library is doing I/O, then IOException has now effectively become unchecked.


The distinction some Java programmers make (including myself) is to treat RuntimeExceptions as indicating interface contract violations (usually preconditions), or more generally, bugs. That is, whenever a RuntimeException occurs, it means that either the caller or the callee has a bug. When existing APIs use RuntimeExceptions to indicate any other error condition, they are wrapped/converted into a checked exception ASAP.

I understand your point about usage-dependend checking. However, I believe it is mistaken. Consider a call chain A -> B -> C -> D, where D throws a checked exception of type X, which C converts into an unchecked exception (still type X). At the same time, B also calls other methods that happen to throw a checked X, and thus B throws a checked X itself, which now, unbeknownst to B, also includes the X from D. B documents the semantic of its X exception, which may not fit the ones thrown by D. Now A catches X, believing the semantics as documented by B, but actually also catches X from D, which was concealed (made unchecked) by C. This breaks checked exceptions in their role as part of an interface contract.

The fact that unchecked exception may originate from arbitrarily deep in the call chain is also the reason why they are unsuitable for defining interface contracts, A function declaring certain semantics for a particular unchecked exception can't realistically ensure those semantics if any function it calls itself must be assumed to also throw exceptions of that type (because it is unchecked). Effectively, you can't safely document "exception X means condition Y" for your function if any nested calls may also throw X for unknown reasons.


Any exception thrown in response to a bug is a fundamental design failure.

At the point where it is evident the program has a bug, there is nothing known, anymore, about the state of the program. It means that data structures defining the state are inconsistent to a wholly unknown degree, and any further computation depending on or,worse, altering that state can do no better than compound the failure.

I always delete code I find that tries to recover from a bug. It is impossible to recover from a bug; the best that can be done is to bail out as fast as possible.


The problem that you outlined is a different one than whether or not exceptions are checked. The further an exception is thrown (call chain depth), the more context that gets lost. This can be fixed with more specialized exception types.

My example of declaring an NPE as checked isn't something I would actually encourage. It conveys very little context, and exceptions which extend RE should signal a bug in the code. I should have selected a better example.


The problem is strongly tied to whether exceptions are checked if you want to have the type system support you in checking and validating your reasoning about the contracts between callers and callees. With unchecked exceptions, unless you have extreme discipline and document exception types for every function, and take care to use dedicated exception types for all non-bug exceptions, effectively emulating what the type-checker does for you with checked exceptions; unless you do all that, it's a fool's errand in terms of ensuring (being able to prove the correctness of) your interface contracts.


Side stepping that you're using a runtime NullPointerException as an example, Java's checked exceptions are just not particularly good at what you want to do.

If you have a function that has an abnormal result as its API, it should have that as part of its return value, because returning as values is what you do with results. Checked exceptions in contrast don't compose. See for example this code:

   X myFunction(Y param) throws MyFailureException();

   List<X> transformed = list.stream()
     .map(e -> myFunction(e))
     .collect(toList());
This is not allowed, because a the lambda in map can't throw. Even if that was allowed map would need a generic "throws Exception" as its API, which would be awful. With checked exceptions the only possibilities you have is catch the exception locally at the point of calling the function or stop as soon as you encounter the failure and bubble it up.

Instead, if you make the part of the return value you can do whatever. I'll use Vavr Try as an example, but you can also do this in Java 17 with sealed interfaces or in a miriad other ways.

   Try<X> myFunction(Y param);

   List<Try<X>> transformed = list.stream()
     .map(e -> myFunction(e))
     .collect(toList());
Now you can still handle failure locally, but you can also check the whole list and make a decision based on that. Or you can propagate the results including failures because this is not the best place to handle them. That's what I mean with composes vs not composes.


I think the question is more on if the implementation of how the jvm does exceptions somehow less affected by core count?

That is, the checked part is just a language implementation, right? The jvm doesn't really make much of a distinction. (This is meant as a check to my assumption.)


Yes, that's right, as static type checks generally are. However, the C++ performance issues are unrelated to whether exceptions are statically and/or runtime checked. Furthermore, Java exceptions are not particularly efficient, in particular because they collect the current stack trace on creation by default, which is a relatively expensive operation.


I thought you could tune away the trace on creation behavior.

Regardless, I'd be interested in seeing if this is a performance bottleneck. I'd guess it is only relevant on dataset processing. Closer you are to a place that legitimately can toss to a user, more likely you are to not care?

That is, if the common case of an exception is to stop and ask for intervention, is this a concern at all?


> I thought you could tune away the trace on creation behavior.

You can when you implement your own exception type, but not in general (and doing so would break too many things).

Exceptions are thrown and caught quite frequently in Java for "expected" cases, for example when attempting to parse a number from a string which is not a valid number. It's generally not a performance problem, and stack trace collection is probably heavily optimized in the JVM. Nevertheless, it's certainly still a lot slower than C++ single-threaded exceptions. You have to realize that even a factor of 100 slower may be unnoticeable for many use cases, because so much else is going on in the application.


Quickly googling, I see the is an option for implicit exceptions to omit stack frame for fast throw. Seems finicky, though.

But, yes, I realize most of these are probably not noticable.


> There's a reason no other languages has them,

As always someone is wrong on Internet.

Java adopted a feature that was initially introduced in CLU, adopted by Mesa/Cedar, Modula-2+ and Modula-3, was being considered for ongoing ISO C++ standardization at the time.


Correct, but also pedantic and doesn't change the point. There's an implied "mainstream" or "currently serious contenders to start new projects in" adjective in "no other languages has them".


Pedantic is good, even C and C++ compilers support it.


"Java's checked exceptions are generally regarded as a mistake."

By people who do not want to believe that errors are part of a system's API and prefer to just write the happy path and let any exception kill the process. And who don't mind getting called at 2:00 am because a dependency buried deep in a subsystem threw an exception that you'd never heard of before.


> errors are part of a system's API

This is a really interesting and nuanced point here. The article linked above [1] talks a bit about it. The problem is, they both are and aren't part of the API in the strict sense.

In the sense that they specify a contractual behaviour they are part of the API of a function. But in the sense that they are something the caller should / needs to specifically care about, they sit in between. That is, in the vast majority of cases, the caller does not care specifically what exception occurred. Generally they want to clean up resources and pass the error up the chain. It is "exceptional" that a caller will react in a specific way to to a specific type of exception. So this is where Java goes wrong because it forces the fine grained exception handling into the client when the majority case (and preferred case generally) is the opposite. It makes you treat the minority case as the main case. There are ways to work around / deal with this but nearly all of them are bad. The article talks about some of the badness.

I do think it's interesting though that Rust has taken off and is generally admired for a very similar type of feature (compiler enforced memory safety). I am really curious how that will age, but so far it seems like it is holding up.

[1] https://www.artima.com/articles/the-trouble-with-checked-exc...


It is still far too early to say.


Errors are part of the system's API, but Java's checked exceptions aren't really that.

Case in point, the many checked exceptions in the Java standard library that are never, ever thrown. Ever. Such as ByteArrayOutputStream#close(). In fact the docs on that method even say it doesn't do anything, and won't throw an exception. But there's still a checked exception that you have to handle, because it came from the interface.

Which is part of why checked exceptions are a mistake. If Java wasn't so aggressively OOP, then maybe there's a good idea there. Like maybe checked exceptions in C++ would work, as you're not relying (as much) on inheritance to provide common functionality. But as soon as interfaces & anonymous types (eg, lambdas) enter the scene, it starts falling over.

Also in terms of API design, checked exceptions are quite limiting. Especially when working with asynchronous APIs, as checked exceptions are inherently coupled to synchronous calling conventions.

And there's also then the problem of not a whole lot of your code base is an "API surface" (hopefully anyway), and it's really vague how a middle layer should handle checked exceptions. Just propagate everything? But that's a maintenance disaster & leaks all sorts of implementation details. Just convert everything to a single type? Well now you can't catch specific errors as easily.


> Like maybe checked exceptions in C++ would work

https://en.cppreference.com/w/cpp/language/except_spec


> By people who do not want to believe that errors are part of a system's API…

The point was that you should be returning errors, not throwing them. Runtime exceptions (null reference, division by zero, out of memory, etc.) ought to indicate a fatal error in the (sub)program or runtime environment. You can trap these, and report them, but it's usually a mistake to try to case-match on them. Unlike errors, which are predictable, enumerable elements of the design, runtime exceptions should be treated as an open set.


I disagree with this. But, I'm also a fan of the condition system in Common Lisp.

That is, if the problem is likely one that needs operator/user intervention, the non local semantics of exceptions makes a ton of sense. Indeed, it is useful to have a central handler of "things went wrong" in ways that is cumbersome if every place is responsible for that.


If you read the article by Anders Hejlsberg, he's not arguing against centralized handling of exceptions—the handling of runtime exceptions is expected to be centralized near the main program loop. That, however, is a general-purpose handler which won't have much logic related to any particular kind of exception; it just reports what happened and moves on. You don't need checked exceptions for that.

The condition system in Common Lisp (which I am also a fan of BTW) is designed around dealing with conditions when they occur, whereas most of the alternatives focus on the aftermath. In particular, conditions don't unwind the stack before running their handlers, which makes it possible to correct the issue and continue, though handlers can naturally choose to perform non-local returns instead. More to the point, there is no requirement to annotate Common Lisp functions with the conditions they may raise, which makes them more akin to unchecked exceptions.


Fair. Sounds like you are more claiming that most functions would be better returning a result type, but some will be better with more?

I view this as I want my engine to mostly just work. It may need to indicate "check engine" sometimes, though. And that, by necessity, has to be a side channel?

I think that is my ultimate dream. I want functions to have a side channel to the user/operator that is not necessarily in the main flow path. At large, I lean on metrics for this. But sometimes there are options. How do you put those options in, without being a burden for the main case where they are not relevant?


> I want functions to have a side channel to the user/operator that is not necessarily in the main flow path.

That is the essence of the Common Lisp condition system, and you can get there in most languages with closures, or at least function pointers, and exceptions or some other non-local return mechanism using a combination of callback functions for the conditions and unchecked exceptions for the default, unhandled case. The key is that you don't try to catch the exceptions, except at the top level where they are simply reported to the user. Instead you register your condition handler as a callback function so that it will be invoked to resolve the issue without unwinding the stack. It helps to have variables with dynamically-scoped values for this, though you can work around the absence of first-class support as long as you have thread-local storage.

C++ actually uses this model for its out-of-memory handling. You can register a callback with std::set_new_handler() to be invoked if memory allocation with `operator new` fails; if it returns then allocation is retried, and only if there is no handler is an exception thrown. Unfortunately this approach didn't really catch on in other areas.


I'm not sure callbacks alone can be equivalent to a resumable conditions system. You really need full coroutines in the general case. Anyway, what you are proposing is more of a partial alternative to exceptions, since the caller has to be aware of what's 'handled' in advance, whereas conditions may additionally unwind up to a predefined restart point or fail up to the caller similar to a non-handled exception.


> I'm not sure callbacks alone can be equivalent to a resumable conditions system.

I agree, but I was not relying solely on callbacks. You do need some form of non-local return (such as exceptions or continuations) to implement the "resumable" aspect with a choice of restart points, in addition to the callbacks.

> You really need full coroutines in the general case.

I'm having a hard time imagining an error-handling scenario that would require the full power of coroutines—in particular the ability to jump back into the condition handler after a restart. In any case, most languages (even C, if you work at it) can express coroutines in some form or another.

> Anyway, what you are proposing is more of a partial alternative to exceptions, since the caller has to be aware of what's 'handled' in advance, whereas conditions may additionally unwind up to a predefined restart point or fail up to the caller similar to a non-handled exception.

Clearly there has been a breakdown in communication, as I though this was exactly what I described. The handler callbacks are "ambient environment" (i.e. per-thread variables with dynamically-scoped values) so there is no particular need for the caller to be aware of them unless it wishes to alter the handling of a particular condition. Restart points can be implemented by (ab)using unchecked exceptions for control flow to unwind the stack, or more cleanly via continuations if the language supports them.


The compiler makes them part of the API. And what a lot of people do is just throw in a bunch of blanket catches with empty code. Although some of this is server vs. desktop software - the article was about complex GUI apps, not long running servers. Tho I personally think long-running servers shouldn't use exceptions. Each call to something out of the running code stack frame should explicitly decide what to do on failure or "didn't hear back." That's how your server gets to be bullet proof (and by bullet proof, I don't mean "auto-restarts on unhandled exception.")




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

Search: