Hacker News new | ask | show | jobs
by jammycakes 1029 days ago
> The unfortunately missing part of exceptions (in mainstream languages) is that they handle this invisibly. Figuring out, at compile time, what sort of exceptions can appear inside a given function is not obvious.

Figuring out, at compile time, what sort of exceptions appear inside a given function is a futile exercise in many contexts, and railway oriented programming does not fix it. Java tried this with checked exceptions and it fell out of favour because it became too unwieldy to manage properly.

In any significantly complex codebase, the number of possible failure modes can be significant, many of them are ones that you do not anticipate, and of those that you can anticipate, many of them are ones that you cannot meaningfully handle there and then on the spot. In these cases, the only thing that you can reasonably do is propagate the error condition up the call stack, performing any cleanup necessary on the way out.

"Handling this invisibly" is also known as "convention over configuration." In languages that use exceptions, everyone understands that this is what is going on and adjusts their assumptions accordingly.

2 comments

> Java tried this with checked exceptions and it fell out of favour because it became too unwieldy to manage properly.

Because they did a half-assed job of it, and required the user to explicitly propagate error signatures. Inference and exception polymorphism are essential.

Checked exceptions always seemed to me to be an exercise of self-flagellation and enumerating badness; when most of the time there are a handful of specific errors that require special handling, with everything else logged/return error/possibly crash.
The problem is that the callee can’t decide for the caller which exceptions will require special handling. And for the caller to be able to make an informed decision about that, the possible exceptions need to be documented. Since this includes exceptions thrown from further down the call stack, checked exceptions are about the only practical way to ensure that all possible failure modes get documented, so that callers are able to properly take them into account in their program logic.
If you want to (and are able to) document all possible failure modes, then checked exceptions will give you that. As far as I can tell, railway oriented approaches can't.

Unfortunately, you can only do that when the number of possible failure modes is fairly limited. In a complex codebase with lots of different layers, lots of different third party components, and lots of different abstractions and adapters, it can quickly become pretty unwieldy. And then you end up with someone or other deciding to take the easy way out and declaring their method as "throws Exception" which kind of defeats the purpose.

No; you simply abstract the underlying subsystem’s exceptions in your own types, the same way you do with any other type.

And yes, “railway oriented approaches” can absolutely do this.

> No; you simply abstract the underlying subsystem’s exceptions in your own types, the same way you do with any other type.

That's all very well as long as people actually do that. It doesn't always happen in practice. And even when they do, the abstractions are likely to be leaky ones.

> And yes, “railway oriented approaches” can absolutely do this.

How? Please provide a code sample to demonstrate how you would do so.

> That's all very well as long as people actually do that. It doesn't always happen in practice. And even when they do, the abstractions are likely to be leaky ones.

They don’t have a choice under “railway oriented” API in a typesafe language — they must translate the subsystem’s error types to their own error type.

If the abstraction is leaky, at least it’s well-specified.

How is that worse than having no abstraction at all, and leaving callers with no idea what error cases an API might raise?

> How? Please provide a code sample to demonstrate how you would do so.

In what language? What data structure?

If we assume Haskell and Either, then it can be as trivial as:

  first mapError someResult
Scala’s Either?

  someResult.left.map(mapError)
You adjust the reported failure modes to the abstraction level of the respective function, wrapping underlying exceptions if necessary. You don’t leak implementation details via the exception types. Callers can still unwrap and inspect the underlying original exceptions if they want, but their types won’t typically be part of the function’s interface contract, similar to how specific subtypes of the declared exception types are usually not part of the contract.