A stack of ten functions wants to handle the error once. Returning it nine times is exactly the type of mind-numbing chore that computers do much more quickly and reliably than people, and I will never understand why so many want not to automate this.
Returning an error through nine levels of functions without doing anything with it anywhere in that call stack would indicate a strong problem with the code architecture to me. Personally I rarely pass on errors "as they are" in Golang anymore, as that makes debugging quite hard. A function receiving an error should either handle it (e.g. by recovering from it or logging it), or at least add relevant context to it so that the user can figure out what's wrong.
I used to like passing errors up the stack like you describe, but this invariably leads to hard-to-debug programs that terminate with unspecific errors like "i/o error", leaving it to the user or developer to figure out in which exact branch of the 9-layer call stack that error originated. If your error handling is exception-based and if your interpreter/runtime generates tracebacks you can do that, but not if you explicitly handle errors within the regular control flow.
The only thing I'm missing there is the filename, otherwise this is a helpful error message since it tells the user what the problem is and at which stage the error occurred. Stack traces, on the other hand, are mostly only helpful to the original developer(s) of a program, they won't help most ordinary users. And while they of course include the calling context they usually don't include other crucial information like variables (e.g. the filename in the example above).
Also, raising and catching exceptions is expensive in many languages. I know that in Java it's fast, but other languages like Python produce significant overhead when creating and raising an exception, that's something one should consider as well.
All you're arguing, then, is that regardless of whether you use error values or exceptions, you need to write good error messages. Hmm, okay. I agree with you on that. But I don't see how that counts against exceptions. Do you think the error message of an exception cannot contain filenames and variable values? And even in cases where they don't (e.g. you're depending on an external library with poor error messages), do you think it's impossible to add context to an exception's error message after it's been raised?
Also, the performance argument counts against your point, not for it. Exceptions rarely happen (when used properly), and when they are not raised, they cost nothing. Whereas go-style errors require explicit checks every single time you execute an operation which can fail.
> this invariably leads to hard-to-debug programs that terminate with unspecific errors like "i/o error", leaving it to the user or developer to figure out in which exact branch of the 9-layer call stack that error originated
You might want to add a caveat to 'invariably', something like: 'provided you're using a language where errors are modelled as one big mashed-together string' ;)
If we assume the basics of how Go is designed are an invariant, I think the best change would be adding syntactic sugar to return a struct satisfying the error interface, annotated with the (statically encoded) symbols and line numbers, and then also the arguments for those function calls. I have never seen a Go codebase whose hand-written 'contextual' error handling blocks provide even this, let alone more useful information. If you have, I'd be interested to hear about it!
Since Go 1.13 wrapping errors is part of the standard library [1]. You could use the "runtime" package to annotate errors with information about the calling context, but IMHO that's not a good idea. I think stack traces should be reserved for unexpected errors, and that is already well covered by the "panic" mechanism. Potentially expected errors that can be handled or returned to the user should simply provide enough information to know what went wrong without forcing the user to go through the code.
I see value in wrapping the error at the source, but I don't see the value in wrapping it beyond that point, unless there's there's genuinely new context to add at some boundary.