Partly, it cluttered the code and decreased legibility.
Mostly, though, it was the debugging ecosystem that was the bigger issue with error handling. With the version I'm writing in PHP/Laravel, stack traces are very clear about where they happen and involving what components. When I plug in Sentry, this makes debugging production issues much easier.
Go's code is more elegant, more performant, and is fun to write, but maintaining PHP in production is much nicer and less time-consuming.
For a project like this, where it's just something I do in my spare time for fun, not having to spend a lot of time managing production is important.
This is a really underrated take on the issue. These “simple” errors become a huge issue when you are trying to track them down. There are no codes or namespaces so you end up having to be really diligent about how you log and wrap them. This can be ok for small projects but once you’ve got a team working on things it can get really frustrating getting everyone on board.
I find C#/ASP.NET Core to be the perfect middle ground. You get very high performance† (at least for such a high level framework), clear error messages, nice debugging, arguably better type system than both of those, and so on. It's fully cross-platform these days, I don't touch Windows at all.
I agree, the lack of stack traces can be annoying when debugging Go programs. Instead of returning raw errors everywhere, the problem can be alleviated by adding some context to returned errors:
if err := foo(x); err != nil {
return fmt.Errorf("foo: bad argument %s", x)
}
The problem with this is, that the caller of the function which returns what you wrote (i.e. a dynamic error message) can't match the error anymore for conditional error handling. That's because errors in go are just strings. I guess a solution could be to provide an error matching function but that seems quite cumbersome compared to typed errors.
This problem has been addressed in Go 1.13 with wrapped errors. If you get an error from a filesystem operation, you can
return fmt.Errorf("ListThings failed: %w", err)
which is a different error type, but the caller can downcast it into the original filesystem error type (even across multiple layers of wrapping) if they're interested in specifically these types of errors.
You should never match against "err.Error()" for conditional error handling, a better solution is to use a custom error type. There are a bunch of different ways to approach this in Go.
You can add stack traces to errors in Go with a few lines of code; there are many existing 'errors' packages which do that. You can also send this to Sentry.
It's funny you use PHP here, as PHP's errors can be problematic in various cases cases on account of making it impossible to get some information from them (like why an fopen() call failed, for example). Never mind the whole "errors" vs. "exceptions" schism (which was improved somewhat with PHP 7, but still has various cases where it's less-than-elegant).
Mostly, though, it was the debugging ecosystem that was the bigger issue with error handling. With the version I'm writing in PHP/Laravel, stack traces are very clear about where they happen and involving what components. When I plug in Sentry, this makes debugging production issues much easier.
Go's code is more elegant, more performant, and is fun to write, but maintaining PHP in production is much nicer and less time-consuming.
For a project like this, where it's just something I do in my spare time for fun, not having to spend a lot of time managing production is important.