Hacker News new | ask | show | jobs
by jerf 2552 days ago
One of the things the Go community has encountered is chaining together too many "if err != nil { return err }" basically returns you to the exceptions problem of having no context around the error anymore, except now you don't even have a stack trace to help you out. (This is what I meant by the bare "if err != nil { return err }" being "exception-y", only, if you like, unambiguously worse if the code base is full of that literal block.)

"Handling" an error includes further annotating it with information about why the code in question couldn't fix it, and this is probably the most common case. (I hedge only because we humans are actually really bad at judging such things, with our availability heuristic bias and other biases. I'm fairly confident this would indeed be the #1 case, but I've been wrong about this sort of thing in the past when I actually went to check.) We don't mean "fix" the error, just... "handle". Ideally you end up with a composite error object (as I said, in conjunction with a few other library-level things that are likely to get pulled up to official support and culture) that contains much more information about the error, and that if you do end up flinging it up to higher level code, you're leaving it with more options for understanding the resulting problems and dealing with them.

1 comments

The task of an error "handling" mechanism could be then described as:

A. Create an error object, enter "error mode".

B. Annotate an error object with contextual information at each call point.

C. Return the error object up the call stack.

D. Translate from "error mode" into "value mode", producing the annotated error object as a value.

For languages with exceptions, B is automatic, but limited to stack traces, C is automatic. A and D are obtained via special language constructs, for example "raise ... / try: ... except E as ex: ...".

For Go [please excuse my almost total ignorance], B is manual, but arbitrarily expressive, C is manual, but terse via "try(...)", whereas A and D are done using a combination of standard language constructs and style conventions.

Assuming the above is correct, perhaps there is some reasonable design that automates B for most common use-cases. In particular, logging the invocation arguments at D makes it trivial to re-run the offending code in a debugger, with full stack trace and invocation arguments. Wrapping most function calls with "try(...)" is annoying, but manageable, whereas thinking what information should be carried by the error object on a case by case basis is a waste of brain cycles.