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

And the other one is the tendency for Go's design to say "exceptions are allowed for me but not for thee".

Yes. Exceptions are kind of a pain, but the workarounds for not having them are worse. Passing back "result" types tends to lose the details of the problem before they are handled. Rust is on, what, their third error handling framework?

Exceptions have a bad reputation because C++ and Java botched them. You need an exception hierarchy, where you can catch exception types near the tree root and get all the children of that exception type. Otherwise, knowing exactly what exceptions can be raised in the stack becomes a huge headache. Python comes close to getting this right.

Incidentally, the "with" clause in Python is one of the few constructs which can unwind a nested exception properly. Resource Acquisition Is Initialization is fine; it's Resource Deletion Is Cleanup that has problems. Raising an exception in a destructor is not happy-making. It's easier in garbage-collected languages, which, of course, Go is. You have to be more careful about unwinding in non garbage collected languages, which was the usual problem in C++.



Internal (bug, e.g. divide by zero or missing function definition in a dynamic language) vs external (out-of-your-control corner case, like file not found) is an important axis.

But another axis is "finality":

1. do you just want to never crash (return code)

2. sometimes crash but have the ability to deal with the problem up the callchain (exceptions in most languages)

3. sometimes crash but be able to fix the problem and continue, at the point the error occured -- not up the call stack (restart-case etc. in common lisp)

4. sometimes crash, but have a supervisor hierarchy make an informed decision if and how to restart you and things in your dependency tree (erlang)

5. crash (panic, assert, exit) and maybe have some less sophisticated but probably very complicated mechanism take care of restarting/replacing you (systemd, kubernetes etc.)

This axis may not be completely orthogonal, but probably mostly is. For example resumable conditions are nice in common lisp both to deal with external stuff (no space left on device? ask user to abort or free some up, and just resume download instead of erroring out as webbrowsers do) but also to just fix problems as you run into them and continue your computation during development, including calling a function you did not define – you can just define it and resume the call to it.

Sadly, the choices in most languages for this second axis are much more constrained. Erlang's supervision trees and common lisp's resumable exceptions in particular seem very useful in many scenarios but nothing else has them (well, elixir has everything erlang has, but it's still the same VM/ecosystem).


>Yes. Exceptions are kind of a pain, but the workarounds for not having them are worse. Passing back "result" types tends to lose the details of the problem before they are handled. Rust is on, what, their third error handling framework?

Rust's non-panicking error handling hasn't really changed: you return a Result<SuccessType, ErrorType>.

What has changed is the details of how to implement your ErrorType. Should it store some sort of context? What useful helper functions can there be? Things like that. What these error handling frameworks provide is macro-based code generation to implement these details, and extension traits for the helper funcitons. They don't change overall method of error handling.

Or, at least, I've not seen one that does.


GP wasn't referring to those types of exceptions. They were referring to the golang authors making escape hatches for themselves which aren't available to end users.


btilly really walked into that one[0], but they meant "exceptions to the rules of the language", not "un-/anti-structured stack and/or control-flow fuckery". Most famously the lack of generics, except for anything the standard library wanted to use generics for.

0: "special cases are allowed for me but not for thee" would have been better but, y'know, hindsight.


This is one of the things Ruby nails with its ensure statement that lets you clean up properly even if an exception was raised.


Rust has panic (and unwind_stack) for when things are truly borked.

Exceptions for error conditions are for the birds because they lead to bugs either in the code or in the compiler, and they lead to messy code.

Rust has functional-style error handling where it's possible to run other code, handle or ignore errors whereas exceptions create messy try catch blocks for every caller.


> Exceptions have a bad reputation because C++ and Java botched them.

Okay...

> You need an exception hierarchy, where you can catch exception types near the tree root and get all the children of that exception type.

Didn't Java do exactly that?


They tried, but the hierarchy is not well-designed. You want a clear distinction between "program has an internal problem" and "external thing (network, file, database, remote service, etc.) had a problem". You usually want to catch "external thing had a problem" in whatever wanted to talk to the external thing. "Program has an internal problem" usually requires restarting the program.


There is a clear distinction. RuntimeException (and descendants) is an internal problem (e.g. divide-by-zero); Error (and descendants) is a VM-internal problem (stack overflow, OOM); checked exceptions are external problems (e.g. IO exceptions).


Yeah... but checked exceptions are controversial within the Java community to say the least. Some codebases eschew checked exceptions altogether, rewrapping any checked exceptions they find.

This is in contrast to how sharp of a divide the community observes around Error vs Exception.

In practice there's a lot of Java code that collapses "internal" and "external" errors into unchecked exceptions.


The blame Java gets for checked exceptions its unfair.

Firstly, CLU and C++ were there first, so the language designers were building on something that they though was a trend that would carry on.

Most never having learned how checked exceptions were done in CLU and C++, blame Java for them.

Then even though it is more convenient to work without them, I do miss in other languages, because developers hate documentation, so I have to keep fixing code or just had a catch all handler, just in case.


I mean personally I happen to agree that Java's checked exceptions represent a reasonable experiment, although I think they suffer from low-level problems, namely the way they interact with lambdas, the way they're at odds with the community's interface-happy habits, and the self-inflicted tension between whether something should be checked or unchecked (should an out of bounds exception on a dynamically sized array be checked or unchecked? It doesn't seem morally all that different from whether a file exists or not).

But language features exist within the programming community that uses them and on codebases where even if you go to the trouble of threading through all checked exceptions underlying code still blows up under you with what should be checked exceptions (which is all production codebases I've ever seen), it quickly becomes an uphill battle to convince team members to use checked exceptions.


There are many Java developers who like checked exceptions. True, there are issues about polymorphizing them (which is what you feel when you use them with lambdas), but they're solvable. As to file vs. array, I do see a fundamental difference. Whether the index is out of bounds or not is up to the program; whether the file exists is not.


There are many who do. There are many who don't and among those who don't they can have pretty strong opinions and be fairly influential (see e.g. Robert Martin's Clean Code). When I say controversial I don't mean that as a back-handed way of saying no one likes checked exceptions, I mean that to say there are large contingents on either side. Unfortunately that means practically speaking checked exceptions lose a lot of their power since you have to proactively rewrap and rethrow a lot of your underlying libraries since a lot of them eschew checked exceptions (e.g. big examples that can pervade entire codebases are Spring and Hibernate). That's not impossible to do (I've done it for personal projects before), but I've never had success in convincing coworkers to do it and you're at the mercy of the accuracy of the documentation (or just seeing it blow up enough times).

I was thinking of the fact that the array can be dynamically sized as not being up to the program, but it's certainly true you can ensure the array isn't out of bounds (just check the array length in a single-threaded context or lock and then check the array length in a multithreaded context) in a way that's not possible for files.

Perhaps a more blurry example is ParseException (checked) vs DateTimeParseException (unchecked). I'm pretty sure I know the reasoning behind it, which is that ParseException is thrown by methods that directly ingest user data whereas DateTimeParseException is presumably not and is probably meant mostly for hard-coded strings. But there doesn't seem to be any real distinction between the two cases; it seems reasonable enough to parse strings passed in by the user as datetimes.

When you solvable, I'm curious do you mean currently in Java or with future features? Because right now if I want to play nice with checked exceptions and lambdas I do an ugly thing of wrapping the exception in a RuntimeException, then catching the RuntimeException, downcasting the wrapped exception, and rethrowing it. It'd be nice to know if there's a better way than that to use.


> When you [say] solvable, I'm curious do you mean currently in Java or with future features?

I meant future releases. That people can manage now make the problem not one of extreme urgency, so we're waiting until there's a solution we like.


Fundamentally, the problem is that whether an exception should be checked or runtime depends on the context. It makes no sense so say that "FileNotFoundException" is checked.

If that exception is thrown because a user selected a file that disappeared, it's recoverable, so it should be checked.

If that exception is being thrown at startup because the app failed to find a file that absolutely needs to be present otherwise everything is broken, then it should be runtime.

Obviously, Java will never be able to adjust to that perspective on exceptions since it would break pretty much everything.


I completely agree that context matters immensely and ultimately it's the caller rather than the callee that should decide whether an exception is fatal.

FWIW though I think that Java the language could adjust to that perspective, if nearly all current exceptions were checked exceptions by default and then callers were expected to wrap as a RuntimeException (perhaps something that was a bit more suggestively named such as FatalException) whatever exceptions they deemed unrecoverable.

Unfortunately there's some implementation-specific problems of going that route (generating exceptions is expensive and you'd probably want a far more fine-grained exception hierarchy rather than lumping whole classes of things under say generic IllegalArgumentExceptions), but they don't seem insurmountable.

More importantly though I agree that I doubt Java the community would ever accept that, at least not for a long long long time.


I suspect the proportion of Java programmers who like checked exceptions even in principle is decreasing over time unfortunately. So waiting might just result in it never being implemented :/ (although I guess that's probably a point in favor of waiting and never expending the wasted energy).

Just looking at the developments of past decade the community seems to be going hard for all exceptions being unchecked. Other statically typed JVM languages (Scala and Kotlin) advertise a lack of checked exceptions as an improvement over Java. Various Java plugins also tout the ability to remove checked exceptions. Manifold is particularly exuberant about it. Lombok offers this capability but is much more reserved about it, in no small part because the way it does so has pretty big downsides.

It's a bit sad (from my tiny personal perspective) because checked exceptions in Java have some ergonomic benefits over Result/Either types in other languages, but social dynamics are what they are.


Maybe I am misunderstanding your point here, but isn't making RuntimeException be the base for internal problems the issue the parent poster was talking about? Divide by zero is an obvious case, but what about when invalid or misconfigured data of some type is passed into a method? This is "internal", ie inside the program, unless I am misunderstanding your terminology? It isn't an external problem, so it isn't a checked exception, which means that the caller to your method doesn't necessarily actually need to handle RuntimeException that you would throw, meaning we're back in the C world of optional error handling?

I might be missing your point, but I would think that making it so that only program-external problems throw checked exceptions defeats a lot of the forced error handling power that checked exceptions grant?


You can always make input validation errors a checked exception with your own exception type. It'll accomplish what you're trying to accomplish. It's not perfectly clean because it'd be nice if you could stick it under the "IllegalArgumentException" part of the inheritance tree, but it does what you're asking for.


Right but if you derive it from Error it isn't checked and same as if it is derived from RuntimeException. Deriving from Exception is checked but then you aren't fitting in with the hierarchy that pron is recommending, or at least what I think they are recommending, which is why I asked for clarification.


Internal errors are bugs or other conditions that the application is not expected to handle (other than, say, restart the thread a-la Erlang). Input validation is not an internal error. IllegalArgumentException is meant to represent bugs, and so an internal error, and so it is unchecked. It does not represent input validation errors.


> Resource Acquisition Is Initialization is fine

It doesn't work well at all for transactions, where both A and B must succeed or neither.

https://dlang.org/articles/exception-safe.html


Thanks, that's a nice discussion!

Do you know if a similar, more granular approach (scope(exit)=~finally, scope(failure)=~catch, scope(success)=else) over go-style defer=~finally is implemented elsewhere than D?




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

Search: