Hacker News new | ask | show | jobs
by lmm 18 days ago
> It's a matter of abstraction and preventing weird coupling.

Does that actually work though? Who ever handles CustomerAccessException specifically, except by calling getCause() and looking at the underlying DatabaseException?

> There are many errors because all errors are contextual! Bad-input in the HTML form-submit is not the same as bad-input in the SQL query. A failed invariant of a tree that somehow made a loop is not the same as a failed invariant of something reporting negative length. An SSH handshake error is not a TLS handshake error.

But the way you handle them is the same. All you can ever really do is a) retry it or b) fail it and alert the developer. And the wrapper doesn't make either of those any easier.

1 comments

Note: This subthread has become about exceptions in general, rather than checked-vs-unchecked. I don't mind, but I wanted to make it explicit.

> Who ever handles CustomerAccessException specifically, except by calling getCause() and looking at the underlying DatabaseException?

Ideally you call a method like `getDetailFoo()`, and using the inherited `getCause()` is for hacky workarounds, like when you need a change in behavior much sooner than Customer classes will be changed to give you the information you need in a proper way.

As I said before about abstraction and coupling, the programmer here shouldn't need to know that SomeDatabase v.1.2.3 is an implementation detail of Customer. Their code shouldn't need to have a direct dependency on some.database.DatabaseException to compile either. Sure, a Java programmer can use runtime reflection instead... but at that point they should definitely be having second-thoughts about whether they're on a path to the Dark Side.

Regardless of how you write the catch-logic, the chained cause remains important for logging and diagnosis.

> But the way you handle them is the same. All you can ever really do is a) retry it or b) fail it and alert the developer. And the wrapper doesn't make either of those any easier.

I'm going to assume this is equivalent to: "All reactions to errors usually fall into a few broad descriptive categories, and encapsulating the original exception doesn't help the handler make precise decisions."

Does that sound right? Because my initial readings went in much less charitable directions like "the person throwing the exception knows better than you do about how you should handle it later" or "all retries are the same basic logic."

> All you can ever really do is a) retry it or b) fail it and alert the developer. And the wrapper doesn't make either of those any easier.

The wrapper is quite important. Imagine these layers exist:

  1. 1-5 different HTTP libraries used by...
  2. 5 different API clients for 5 different fortune cookie service, used by...
  3. A library that promises a wide and resilient range of fortunes by putting many sources (today, 5) together and semi-randomly picking between them.
  4. The website you're making that shows fortunes when people log in.
Do you want layer #1 exceptions to travel all the way to #4 as-is, so that you get an HTTP 500 and you won't actually know which remote service is at fault? (Without grepping the stacktrace, which is *ick*.)

No, because either #2 or #3 should be wrapping the HTTP exception in a new one that can carry that extra "which one did I randomly pick" information in a defined way.

Do you want a #2 exception to travel to #4 without changing, so that you have to write catch-clauses (or reflection) for 5 or more exceptions from the 5 different services, code that will be wrong when a version update turns it into 7?

No, because #2's job is to abstract those N services away and throw its own much set of exceptions to cover common cases.

So after all that I realize I didn't specifically address retry-vs-fail choices, but I think this still sufficient to show that layering is necessary.

> Ideally you call a method like `getDetailFoo()`

I know that's the theory, but have you ever seen it work that way in practice? Because I haven't.

> Regardless of how you write the catch-logic, the chained cause remains important for logging and diagnosis.

Sure. But if it's not being caught and handled but only logged or investigated then you're not gaining anything from the wrapper.

> Do you want layer #1 exceptions to travel all the way to #4 as-is, so that you get an HTTP 500 and you won't actually know which remote service is at fault? (Without grepping the stacktrace, which is ick.)

Yes? If it's a fail-and-alert-the-developer case then I'm going to look at the stacktrace first anyway.

I might have retries at any layer, but I don't see any case where #4 uses information from a wrapper exception from #3 to make a decision. The only thing #4 is going to do with a failure from #3 is retry or fail, and it's not going to make a different decision based on which service it was. (Maybe you want metrics or circuit breakers on the individual fortune cookie services, but in practice what I've seen is that you'd always find a way to plumb them in at layer #1 or #2, by reflection if need be, rather than have them work of the exceptions coming out of #3. I know the theory you're talking about, I've just never seen it actually work that way)