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

I'm disappointed the Java has given exceptions such a bad wrap that all new programming languages dance around them like crazy. I understand Rust going with error returns due to it's low-level nature but Go is pretty high level.

This semi-explicit error handling code optimizes for fairly trivial cases as very few real-world functions don't have some kind of exception condition. The difference between prefixing nearly every function with "check" vs. having that implicit is very small.

Proper use of exceptions puts the focus on where you can handle the error rather than where the error occurs. Java screws this up with checked exceptions, which puts the all focus on error site, and programmers have subsequently determined (correctly) that checked exceptions are hardly better than error returns.

However, that is fundamentally the wrong way to look at error management. If a method 17 levels deep on the call stack throws a network error, I shouldn't have to care about those 17 levels because I'm going to restart the whole operation at the point I can do that. The important part is not where the error is raised/returned.



Exceptions are second only to proper return types for error handling such as Result<T, E> or Option<T>.

And in order to have those you have to have generics. Once you have generics result types can be used for 99% if error handling.


No, because you are still bubbling these 17 stack frames, as OP was (rightfully) complaining about.

The only downside of exceptions is the loss of equational reasoning, really.

But for everything else, they are a superior approach to result values.


It's not really about Java giving exceptions a bad rap. After all exceptions are a feature of almost any OOP language designed after 1990 and they work nearly the same way in all of them.

I've argued in the past that the reason the current crop of languages that compile to native code tend to avoid exceptions, is to do primarily with implementation cost, the backgrounds of the language designers and a mistaken attempt at over-generalisation in an attempt to find competitive advantage.

https://blog.plan99.net/what-s-wrong-with-exceptions-nothing...

One problem is that the sort of people who write native toolchains that compile to LLVM or direct to machine code like Go, tend to be the sort of people who spent their careers working in C++ at companies that ban C++ exceptions. So they don't really care or miss them much.

Another is complexity. One of the most useful features of exceptions is the captured stack trace. But implementing this well is hard, because generating a good stack trace in the presence of optimised code with heavy use of inlining requires complex deoptimisation metadata and many table lookups to convert the optimised stack back into something the programmer will recognise. Deopt metadata is something the compiler needs to produce as it works, at large cost to the compiler internals, but most compiler toolchains that aren't optimising JIT compilers (like the JVM) don't produce such metadata at all. So stack traces in natively compiled programs are frequently useless unless you compiled in "debug mode" where all optimisations are disabled. VMs like the JVM have custom compilers that generate sufficient metadata, partly because they have more use for it - they speculate as they compile and use the metadata to fall back to slow paths if their speculations turn out to be wrong. But a language like Go or Rust doesn't have a runtime that does this.

Finally, I find many of the arguments cited against exceptions to be very poor.

For example the Go designers cite Raymond Chen's blog posts as evidence that exceptions are bad because it's easy to forget to handle errors and the checks are "implicit". This seems totally backwards to me.

You cannot forget to handle an exception in most languages that use them. If you forget a method can throw and don't catch it, the runtime will catch it for you and print out a stack trace and error message that tells you exactly what went wrong and where. The thread won't continue past the error point and the runtime will stop the thread or entire app for you. But if you forget to check an error code, the error is silently swallowed and no diagnostics are available at all. The program will continue blindly in what is quite possibly a corrupted state.

The Go designers argue that with exceptions you can't see "implicit checks". This is an odd argument because, beyond not being able to see if you are ignoring a return code, all programs must have implicit checks of correctness. For example checking for null pointer dereferences or divide by zero conditions, yet it makes no sense for e.g. divide to be a function that returns an error code you must check every time. In C, such checks turn into signals that can be caught or on Windows, the OS delivers them as exceptions! Yes, even for C programs, it's called SEH and is a part of the OS API - all programs can have exceptions delivered to them at any point. If you don't catch them then the OS will catch it for you and display the familiar crash window. UNIX uses signals which are not as flexible but have similar semantics; if you don't catch the signal you'll get the OS default crash handler.

So this is no knock against exceptions. Moreover, very often you don't want to handle an error. Not all code can or should attempt to handle every single thing that can go wrong at the call site, or even at all. So in practice errors are almost always propagated back up the stack, often quite far up. For many errors there's nothing you can do beyond the default exception handling behaviour anyway of printing an error or displaying a crash dialog and quitting, e.g. many programs can't handle out of memory or out of disk conditions and nor would it make sense to invest the time in making them handle those conditions.

Exceptions were developed for good reasons; they were designed in response to the many enormous problems C-style error handling created and codified common patterns, like propagation of an error to the point where it makes sense to capture it, changing the abstraction level of errors and so on.

The Go designers have realised what many people told them from the start - that their error handling approach is bad. Unfortunately they don't seem to have taken the hint and reflected on why they made these bad decisions in the first place. Instead their language comparison ignores all the other languages that went a different direction, and only looks at Swift and Rust. Neither language is especially popular. Other modern languages like Kotlin which do use exceptions are completely ignored.

In conclusion, nothing in this document makes me think that Go is likely to fix its self-admitted design errors. They're probably just going to make variants of the same mistakes.


> It's not really about Java giving exceptions a bad rap. After all exceptions are a feature of almost any OOP language designed after 1990 and they work nearly the same way in all of them.

Java has checked exceptions which have been universally agreed upon as bad idea. No subsequent languages use them and they've even been removed from C++. Unfortunately most common first programmers' language is Java and they learn the worst possible implementation of exceptions. All your pro-exception arguments (and all mine) pretty much all apart when dealing with checked exceptions. So it's easy to see why anyone who's only exposure to exceptions was from Java would consider error returns the superior option.

> But implementing this well is hard, because generating a good stack trace in the presence of optimised code with heavy use of inlining requires complex deoptimisation metadata and many table lookups to convert the optimised stack back into something the programmer will recognise.

Of course, the only reason any of this is necessary is because compilers don't implement exceptions in naive way of simply returning and propagating the exception the same way you would propagate an error return. Instead they go through a convoluted mess to ensure that in the case the exception doesn't occur, you pay no run-time performance penalty. So, in fact, this is still an advantage of exceptions over error returns. This is merely an optimization that is possible when error handling is not explicit.


That's all true. I've become more sympathetic to checked exceptions over time though. I think the issues Java had with it are more to do with poor choice of what to make checked in the standard library. For example IOException should not have been made checked. Also, it's too difficult to change an exception from checked to unchecked, and it should have created suppressable warnings rather than compiler errors.

It's a pattern - early versions of Java erred too much in favour of making things compile errors rather than warnings, like unreachable code being an error , even though it's common whilst debugging or writing code (Go made the same mistake). Not catching checked exceptions is something very common whilst making prototype code or simple command line tools where all you can do with most errors is print the message and quit anyway. It isn't worth forcing the developer's hand via an error.

But I won't be surprised to see some variant of checked exceptions be explored again in the coming years, probably with IDE support. Being able to know how a method can fail in the type system, beyond just checking the docs, is a very useful thing if the information is used in a reasonable way.


Checked exceptions fundamentally violate most of the principles of object-oriented programming by forcing you to expose implementation details into the method type signature. Encapsulation, abstraction, and even polymorphism are violated by this.

Java programmers typically get around this by creating ClassNameException classes and then stuff the real exception (untyped) into the innerException property.

Checked exceptions are conceptually and fundamentally flawed.


People are often lazy and do that, but it's not fundamental. You can and should hide exceptions behind more abstract types if you're using layers of libraries, but that happens with error codes too - a curl error code meaning "server returned 500" might get turned into a different error code meaning "could not download data file", losing the exact nature of why it failed because the library doesn't want to expose curl's constants in its own API.

The big win of Java-style exceptions in this situation is there's a common protocol for chaining, so you can make more abstract errors but without losing any detail about what the original cause was. I find this helps me almost every day.


> Checked exceptions fundamentally violate most of the principles of object-oriented programming by forcing you to expose implementation details into the method type signature

No, they don't. They can be abused in a way which does this (and can be implemented in a way which unnecessarily creates avenues for it), but checked exceptions in general are isomorphic to static return types, and don't violate OOP any more than static return types do.


Static return types would violated OOP as well if they exposed implementation details unrelated to the result of the operation.

If you have a method called GetPrice() that uses a database, you know it, because you have to expose all the possible exceptions. If you change that to use a file or a network service, you have to change every caller and every caller of that. Is that abstraction? No. Is it polymorphism, also no.

You can't have two different implementations of GetPrice() that are compatible unless they have exactly the same exception result. What's the value of that?


> Static return types would violated OOP as well if they exposed implementation details unrelated to the result of the operation.

You're arguing against a particular usage of checked exceptions. You don't have to use checked exceptions like that so the argument is not convincing.

> If you have a method called GetPrice() that uses a database, you know it, because you have to expose all the possible exceptions.

And if you had a method called getPrice() that should always fail if a product is part of a bundle and cannot be purchase separately then you would potentially want every caller to be aware of that possibility. One error condition is an implementation detail and the other is a fundamental invariant of the domain. Guess which one checked exceptions should be used for?

> You can't have two different implementations of GetPrice() that are compatible unless they have exactly the same exception result. What's the value of that?

That's the whole point. There are invariants that should be communicated in a type-safe manner. The confusion here isn't new and is actually well understood. All error signals fall into two buckets -- does my caller care and can she respond sensibly to it or will she just pass on the error and/or quit. The benefit of checked exceptions is maximum flexibility: you can guess (and it's always a guess) that your caller doesn't care and throw an unchecked exception or, if you feel really strongly that the caller should care because some key invariant has been broken, you can throw a checked exception. It's not an exact science but this question -- does my caller care? -- goes to the very heart of encapsulation and abstraction.


I disagree. If you have an polymorphic getPrice() as part of an interface than you can't possibly list all the invariants as individual well-typed exceptions. That would assume perfect knowledge of all possible implementations of that method now and in the future. That's not OOP.

In a non-polymorphic case of a bundled item, you'd simply not have a getPrice() method on the item at all. It might simply not implement the PricedItem interface, for example. There are plenty of ways of ensuring all invariants are met, in a type-safe way, without using exceptions. If it's possible to come to situation as normal correct operation (such as an item in bundle) then it's not exceptional.


> Static return types would violated OOP as well if they exposed implementation details unrelated to the result of the operation.

Sure, but neither checked exceptions nor static return types inherently do that.

Bad practices around either can, and bad language design around either can make that more likely. (I honestly think unchecked exceptions are a bigger problem, because they make poor chocies less obvious.)

> If you have a method called GetPrice() that uses a database, you know it, because you have to expose all the possible exceptions.

Only if you are letting them bubbled up as is; you only have to expose exceptions you don't handle, which is equivalent to if you shouldn't expose an exception to a caller, you must handle it, perhaps with a handler which simply throws an exception that is appropriate to expose.


> you only have to expose exceptions you don't handle

Which is the vast majority of exceptions because by-definition exceptional conditions can't be handled within the method. That's why they are exceptions.

The presumption that most exceptions are handled locally is wrong. Exceptions, if they are handled at all, are almost always handled far away from the site they were thrown. Further it doesn't even matter which method it is! I can handle all network errors by restarting the operation from the start; I don't need to know which of the 3,000 possible methods up from my loop actually triggered the network exception. If any of those methods change from throwing to not throwing a network error, it literally doesn't matter. But I do need to know it's a network error and not some abstracted LibraryNameException.


> But I do need to know it's a network error and not some abstracted LibraryNameException.

How is that any different from using return values like Rust does? It seems that libraries have their own `LibraryError` type which you have to decode anyway.


> That's all true. I've become more sympathetic to checked exceptions over time though. I think the issues Java had with it are more to do with poor choice of what to make checked in the standard library.

Exactly this. Working on large codebases has made me a fan of checked exceptions. It's certainly the most efficient and elegant mechanism available to communicate real domain violations. Java's mistake was that the vast majority of checked exceptions (IOException, SocketException etc) are clearly RuntimeExceptions.

> But I won't be surprised to see some variant of checked exceptions be explored again in the coming years, probably with IDE support.

The logical conclusion of checked exceptions is Design By Contract. It would be great to see checked exceptions expressed as real invariants. The syntax might be something along the lines of:

'cancel(Order o) throws TooLateToCancelException if "!o.isWorking()" { ... }'

Because checked exceptions are required to express real domain invariants you might as well go all the way and capture those invariants too.


I think they had a valid point that exceptions and their implicit error checks do make it hard to look at the program text and see the error-handling program flow.

However, for languages with exceptions, it makes me wonder if perhaps IDEs and other tools could take up the slack here. If I call a function that was declared to throw an exception, why not visually annotate that as a place where an error may bubble up from? The information is there in the definition, but it'd be very handy to see it at the call site.


Just assume that all methods do, or could potentially, or will in the future throw an exception and you'll have no problem looking at program and understanding the error handling flow.

There is a consistent focus on the site the error is generated but that's the least important piece of information for handling errors.


Isn't Kotlin limited to supporting Java's exception model, though?


There's a Kotlin/Native that uses LLVM as its backend and differs from Kotlin/JVM in several key ways. I guess it could do something different but as far as I know it has exceptions.

Also Kotlin doesn't have checked exceptions at all.


Java's explicit exceptions design is based on Modula-3, CLU, C++.




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

Search: