In the context of server code, that's really the fatal problem. And in general, "scream and die" isn't necessarily the optimal case either; not everything is a consumer web app where showing an error page is perfectly fine (I mean, not good, but generally fine). Knowing what can throw an error where is really useful.
I don't consider the error question settled yet. I think the failure of checked exceptions is instructive, because they clearly demonstrated that there's this "unioning" effect where a bit of code's error throwing capabilities is the union of all code it may call (in the absence of a programmer carefully restricting it), and seemingly minor bits of code may in fact result in "union"ing in a whole whackload of other errors. I'm not convinced anything has fully managed to address this yet. Unchecked exceptions tend to just cover over the problem, but don't help you deal with it much. A closed sum type for errors is great when you can use it, but it composes poorly if you start trying to move that around the system, and in practice Haskell still has exceptions. Using arbitrary or nearly-arbitrary terms for errors like Go (error is an interface) and Erlang do means you can pass errors back up the stack easily without the type system complaining and it's not a "hidden goto", but it means there's no way to be really sure that you've actually handled all possible errors in the best way. There's still no way to point at a block of code and assert that you know all the errors it can generate.
I think part of my willingness to accept how Go does errors is that I lack the belief that it's a solved problem and "duh, just use $SOLUTION". There's only some solutions that do a good enough job of sweeping the issue under the rug that they've convinced you it's solved, but the issues are still waiting to pop out as soon as someone stomps their boot down in the right place. It still seems like there's more work to be done here.
Sometimes a bit of dynamic scoping can go a long way.
And, really, the error-handling part fits fairly well with Go (not only what the language supports -- which is, after all, full-featured exceptions with a different [IMO, improved] syntax -- but also with the conventions on their use across public API, though that requires some thought about the motivation for that rule and how it applies to that kind of function.)
OTOH, Go's type system, as opposed to its error-handling approach, is a real problem for generic functions. Though if you just mean non-generic higher-level functions, this is less of an issue.
> You have to make sure to thread error handling through everything that would ever take another function as an argument (since the error handling characteristics of the unknown function are open)
No, you just have to state as an assumption of the "generic" that any function it works on is wrapped to panic in the event of error, and the caller needs to handle the panics. To fit with the Go convention on panics and public APIs, you should wrap any function that results from applying such a higher-level function to a function that an fail into another function that recovers from panics and provides error returns before passing the resulting function across a public API boundary.
Without generic types, I don't think generic programming is very common in Go and exceptions won't be missed much yet. However, if they ever get around to adding them, I expect dominoes to fall :)
> To fit with the Go convention on panics and public APIs, you should wrap any function that results from applying such a higher-level function to a function that an fail into another function that recovers from panics and provides error returns before passing the resulting function across a public API boundary.
So panic is basically an exception but in callback form rather than block form? Holy Hollywood principle! I'm not sure what you can hope to accomplish in a callback that would make the immediate calling context sane again.
Exceptions won't be missed at all, with or without generic programming, because, and I get tired of saying this, Go has exceptions. panic/recover/defer provide all the functionality of raise/catch/finally -- the main difference in structure being that (1) the context is always a function, not some other block within a function, and, (2) "recover" is done within a deferred function (loosely parallel to an "finally" block) as opposed to "catch" which is done in a sibling block to "finally". [1]
> So panic is basically an exception
Yes.
> but in callback form rather than block form
No.
And what is the equivalent of catch if we have an equivalent of finally? It seems to be recover, which allows a deferred execution to query what exception has occurred, correct? Is there a good discussion of why this is better than try/catch/finally?
How is function structure any more complex than block structure?(I mean, visually, sure, there are potentially a few extra sigils, but structurally how is a block different than a nullary function?)
> And what is the equivalent of catch if we have an equivalent of finally?
Recover within a deferred function provides the ability to do the functionality of catch -- I wouldn't call it a one-for-one equivalent (the combination of features is equivalent, but there isn't a one-for-one correspondence between the individual features except between panic and raise/throw.)
Its potentially more flexible, in that it doesn't restrict the "generic" cleanup code to run strictly after the exception handling code; this also probably makes things a bit more clear in the instances where you want to apply logic in the guaranteed-closeout that might affect return values but needs to consider errors in the event they occurred.
The thing is, if you're playing fast and loose, exceptions are great; there's no extra work, and it blows up if something goes wrong, so you find out -- great!
But if you're trying to do the best possible thing in every scenario, it becomes much harder.
(E.g. maybe for some reason a field in a record you're loading isn't valid UTF8. Let's say it's in the context of a larger request -- you still want the request as a whole to succeed; the non-valid UTF8 isn't a critical issue, just something you should log and fix later. In Python land, it's quite possible you wouldn't even realize that an exception could be thrown there. You leave your code by a totally unexpected exit. Users are seeing errors for no particularly-great reason. So: you fix things to handle this case properly, and you end up with ugly try:catch: indentation that's worse than just checking error return codes in the first...)