Hacker News new | ask | show | jobs
by teddyh 428 days ago
> Lack of Type System Integration

Well, IIUC, Java had (and still has) something called “checked exceptions”, but people have, by and large, elected to not use those kind of exceptions, since it makes the rest of the code balloon out with enormous lists of exceptions, each of which must be changed when some library at the bottom of the stack changes slightly.

5 comments

> each of which must be changed when some library at the bottom of the stack changes slightly.

I hate checked exceptions too, but in fairness to them this specific problem can be handled by intermediate code throwing its own exceptions rather than allowing the lower-level ones to bubble up.

In Go (which uses error values instead) the pattern (if one doesn’t go all the way to defining a new error type) is typically to do:

    if err := doSomething(…); err != nil {
      return fmt.Errorf("couldn’t do something: %w", err)
    }
which returns a new error which wraps the original one (and can be unwrapped to get it).

A similar pattern could be used in languages with checked exceptions.

The biggest annoyance with Java checked exceptions IME is that it’s impossible to define a method type that’s generic over the type of exception it throws.

Checked exceptions should indicate conditions that are expected to be handled by the caller. If a method is throwing a laundry list of checked exceptions then something went wrong in the design of that method’s interface.

> The biggest annoyance with Java checked exceptions IME is that it’s impossible to define a method type that’s generic over the type of exception it throws.

Exactly. If Stream methods like filter() and map() could automatically "lift" the checked exceptions thrown by their callback parameters into their own exception specifications, it would solve one of the language's biggest pain points (namely: Streams and checked exceptions, pick one).

I like checked exceptions in general, but I agree, they didn't play well with the Streams API.
> it makes the rest of the code balloon out with enormous lists of exceptions

That's mostly developer laziness: They write a layer that calls the exception-throwing code, but they don't want to to think about how to model the problem in their own level of abstraction. "Leaking" them upwards by slapping on a "throws" clause is one of the lowest-effort reactions.

What ought to happen is that each layer has its own exception classes, capturing its own model for what kinds of things can go wrong and what kinds of distinctions are necessary. These would abstract-away the lower-level ones, but carrying them along as linked "causes" so that diagnostic detail isn't lost when it comes time for bug-reports.

Ex: If I'm writing a tool to try to analyze and recommend music that has to handle multiple different file types, I might catch an MP3 library's Mp3TagCorruptException and wrap it into my own FileFormatException.

It is laziness to an extent, sure, but that's a huge part of language design. We wouldn't use Java or C# or Python or any of these high level languages if we weren't lazy, after all, we'd be writing assembly like the silicon gods intended!

The problem with Java checked exceptions is they don't work well with interfaces, refactoring, or layering.

For interfaces you end up with stupid stuff like ByteArrayInputStream#reset claiming to throw an IOException, which it obviously never will. And then for refactoring & layering, it's typical that you want to either handle errors close to where they occurred or far from where they occured, but check exceptions forces all the middle stack frames that don't have an opinion to also be marked. It's verbose and false-positives a lot (in that you write a function, hit compile, then go "ah forgot to add <blah> to the list that gets forwarded along..." -> repeat)

It'd be better if it was the inverse, if anything, that exceptions are assumed to chain until a function is explicitly marked as an exception boundary.

When I say lazy, I mean the essential work of modeling what's going on and making a decision which can only be made by a human. In this respect, choosing what exceptions-types to throw is like choosing what regular-types to return. If I return a GraphNode instead of a DatFile, then I should probably throw a GraphNodeException instead of a DatFileChecksumException.

Syntactic sugar should make it easier to capture the decision after it's been made. For example, like replacing "throws InnerException" (perhaps a leaky abstraction) with something like "throws MyException around InnerException".

Yes but you only make those types of decisions on library boundaries, which is a relatively small amount of code. Meanwhile checked exceptions make all of the code harder to deal with in non-trivial ways (eg, the ubiquitous "Runnable" cannot throw a checked exception). And it's that everywhere-else where "laziness" won and checked exceptions died.
I think it's fair to say that having some sort of syntactically lightweight sum or union type facility makes this way nicer than anything Java ever had -- subclassing isn't really a solution, because you often want something like:

    type FooError = YoureHoldingItWrong | FileError
    type BarError = YoureHoldingItWrong | NetworkError
    fn foo() -> Result<int, FooError> { ... }
    fn bar() -> Result<int, BarError> { ... }
    fn baz() -> Result<String, BarError> { ... }
TypeScript's type system would hypothetically make this pretty nice if there were a common Result type with compiler support.

Rust needs a bit more boilerplate to declare FooError, but the ? syntax automatically calling into(), and into() being free to rearrange errors it bubbles up really help a lot too.

The big problem with Java's checked exceptions was that you need to list all the exceptions on every function, every time.

Java's sealed interfaces enable typing errors.

https://blogs.oracle.com/javamagazine/post/java-sealed-class...

Although syntactically lightweight it is the opposite of.
I agree; Java is constitutionally incapable of being lightweight. I much prefer Typescript's union syntax. I'm glad Python copied it.
records are lightweight
I love libraries that does a simple check and signals that it "failed" with ThingWasNotTrueException.

In surprising twist: Java has ConcurrentModificationException. And, to counter its own culture of exception misuse, the docs have a stern reminder that this exception is supposed to be thrown when there are bugs. You are not supposed to use it to, I dunno, iterate over the collection and bail out (control flow) based on getting this exception.