Hacker News new | ask | show | jobs
by win311fwg 14 hours ago
> In the overwhelming majority of cases, you want to trivially send the error up the stack to be handled

There is no case where you would ever trivially send the error up the stack.

First, if your function doesn't have any meaningful information to add, you need to seriously question why the function exists. It is almost certainly not adding anything of value and the function that originally produces the error can just as well be called directly. No need to trivially pass an error when the useless "middleman" function doesn't exist.

Second, even if you still decide that a function that doesn't have worthwhile information is still worthwhile having, if you pass someone else's error straight through then you become forever coupled to the implementation of someone else. If you, for example, pass a SQLite error and then later decide you want to use Postgres, now you have to emulate SQLite errors every time Postgres produces an error, bringing you back to square one while also having one hell of a confusing API. Unless you hate other programmers and are purposely trying to make their life a living hell, the only sensible approach is to produce new error values at each step so that you can maintain stable sentinels/types.

A good language solution for errors might make it look like you are trivially passing an error up the stack while using some other method to attach the necessary metadata, like we see in some other languages, but nobody has yet figured out how to make that fit into Go. It is generally agreed upon that Go would greatly benefit from this, but until someone rolls up their sleeves and solves it...

1 comments

Functions exist first to give names to conceptual blocks, and should generally be defined by "level of abstraction." i.e. you give a name to some task that then perhaps calls some low level data validation or manipulation functions which can return errors. Your business process might not care which task had an error, so it just bubbles up to the higher level coordinator (e.g. a handler just returning errors to a router).

You do not need to have new wrappers at every level. That's what interfaces are for. e.g. if all you're going to do is retry up to N times and log, and you don't need special handling, you just call String() or whatever. But you still want libraries to return domain errors so that you can handle them if you need to, or for tests, etc. And you can have some layer in the middle to make like SQLError or DatabaseError.

Right I'm saying Scala does exactly that, and has libraries that e.g. propagate stack traces across fibres automatically and invisibly. There's prior art to observe. It's not some mysterious unsolved problem.

> Functions exist first to give names to conceptual tasks

Right, and conceptual tasks will almost always have information that is usefully attached to an error. There are exceptions, but even in those exceptions you still have to ensure you don't leak implementation details, so you still cannot trivially pass the error through.

> Your business process might not care which task had an error

That's true, but you would never write upstream functions with the business process in mind. That is for the downstream functions to determine. Doing so would couple an upstream function to the business process, making it difficult (maybe even impossible) to use again when the next business process wants to reuse it, which would be complete insanity. A function is written to provide what makes the function useful for the sake of the function itself. It is up to the caller to decide what facets of that are useful for the business process being implemented downstream.

> You do not need to have new wrappers at every level.

You absolutely do need them. The language can work to hide that they are being created, but that does not preclude the necessity of them. This is most definitely a solved problem in other languages, but it remains that nobody has figured out how to take those ideas and apply them in Go. What was done in another language cannot be lifted wholesale and simply dropped into Go. The world is not quite so simple. It remains an unsolved problem in Go.

And understandably so. Who has time to work on solving the problem when there are comments on HN complaining that nobody has solved it yet to be made?