However, I wouldn't pick a language purely based on its error handling capabilities. That's treating everything like a nail just because you have a hammer. I'd pick a language that's suitable for the task at hand. Go is suitable for making small(ish) webservices. Over 10k lines of code it becomes really hard to keep things straight. However, that's more due to its very limited scoping abilities.
As far as Go is concerned, you can make the error handling work. In ContainerSSH, we built our own logging overlay, which you can find here: https://github.com/ContainerSSH/libcontainerssh/tree/main/lo... This companion message library has a custom error structure that carries along an error code, which uniquely allows identifying the cause of the error: https://github.com/ContainerSSH/libcontainerssh/blob/main/me... Errors can be wrapped and we added tools to determine, if a certain error has an ancestor with a specific code, allowing for tailored error handling cases. We also added a tool that gathers the comments from the error code constants and adds them to the documentation: https://github.com/ContainerSSH/libcontainerssh/blob/main/cm...
I hope this helps.
Could you please elaborate more on this?
In larger projects, especially with lots of contributors, I found that you'll want to practice some level of defensive programming. In other words, you'll want to make sure that parts of your program only talk to each other via "sanctioned" APIs. Go makes this difficult because anything in the same folder can access data in structures defined in the same folder.
For example, the Kubernets backend for ContainerSSH (https://github.com/ContainerSSH/libcontainerssh/tree/main/in...) has two parts, one dealing with the integration with the rest of ContainerSSH, while the other deals with Kubernetes. In order to provide some level of separation, we added an interface in the middle to document the package-internal API somewhat: https://github.com/ContainerSSH/libcontainerssh/blob/main/in... However, this is suboptimal and leads to unnecessary boilerplate code.
You could, of course, forego the interface, but that would mean that code reviewers would have to make sure nobody is taking the easy route and messing with the internal state of a struct that they have no business messing with. In other words, you'd want some level of encapsulation which is not easily doable in Go.
In other languages you have much more granular visibility options, either in an OOP way (private, protected, public, package-private, friend classes, etc), or by providing things like header files that describe an interface between parts of the application.
Is there a language that has error handling "done well " that you like?