And Java typically does produce both (see Exception "cause" field). So when an exception stack trace is printed it's actually list of stacktraces, for each "cause". You can skip stacktraces and just concatenate causes' messages (like people often do in Go).
So the full message would be like "Cannot add item X to cart Y: Error connecting to warehouse Z: Error establishing TLS connection to example.com 127.0.1.1: PKIX failed".
Exceptions with stack traces are so much more work for the reader. The effort of distilling what's going on is pushed to me at "runtime". Whereas in Go, this effort happens at compile time. The programmer curates the relevant context.
And come on, skipping 5 lines and only reading the two relevant entries is not "much work". It's a feature that even when developers eventually lazied out, you can still find the error, meanwhile you are at the mercy of a dev in go (and due to the repeating noisy error handling, many of the issues will fail to be properly handled - auto bubbling up is the correct default, not swallowing)
The Go errors that I encounter in quality codebases tend to be very well decorated and contain the info I need. Much better than the wall of text I get from a stack trace 24 levels deep.
Quality java code bases also have proper error messages. The difference is that a) you get additional info on how you got to a given point which is an obviously huge win, b) even if it's not a quality code base, which let's be honest, the majority, you still have a good deal of information which may be enough to reconstruct the erroneous code path. Unlike "error", or even worse, swallowing an error case.
This is why stack traces exist. But I agree Java seems to not really have a culture of “make the error message helpful”, but instead preferring “make the error message minimal and factual”.
For what it’s worth, the rise of helpful error messages seems to be a relatively new phenomenon the last few years.
And that's why you should have multiple appenders. So in code you write "log.error("...", exception)" once, but logging writes it in parallel to:
1. STDOUT for quick and easy look, short format.
2. File as JSON for node-local collectors.
3. Proper logging storage like VictoriaLogs/Traces for distributed logging.
Each appender has its own format, some print only short messages, others full stacktraces for all causes (and with extra context information like trace id, customer id etc). I really think STDOUT-only logging is trying to squeeze different purposes into one unformatted stream. (And Go writing everything to STDERR was a really strange choice).
This is the kind of scenario that is served better by Go/C-style error values than exceptions. Error values facilitate and encourage you to log what you were doing at the precise point when an error occurs. Doing the same with exceptions idiomatically often requires an exception hierarchy or copious amounts of separate try/catches.
The difference really becomes apparent when trying to debug a customer's problem at 3am (IME).
Your stack trace tells you where in the code the error occurred, but doesn't tell you what it was doing with what data. For that you need to pass context for the error up the chain of calls, adding to it as you go up. Exceptions are not a great way of doing it as you only have the local context, which isn't a great help when you're catching N levels up.
And if you're not catching N levels up but catching at each level, then you are emulating error values but with try/catch blocks.
That is great for you as a developer. As a sysadmin supporting other people's crap, stack traces and heap dumps are useless beyond forwarding them to the vendor.