Resources that need to be closed at function exit must be wrapped with `using` or `try/finally` in Java; with `defer` in Go.
Functions that can handle specific errors from a sub-function need to know about that error type in Java, error type or error value in Go.
Functions that can't handle any specific errors from their callees don't need to do anything in Java; they need to propagate any error in Go.
It's true that in Java it's less clear where a function can end, since any statement can potentially throw. This is technically true in Go as well with panic(), but let's accept that that is much more rarely used. But either way, if the function has any cleanup to do, that cleanup must be done in try/finally or defer, otherwise the function is brittle. So, why do I care if the function can throw an exception in the middle?
1. https://www.infoq.com/news/2019/07/go-try-proposal-rejected/
For example, this code:
void foo() {
var res = acquireResource();
useResource(res);
releaseResource(res);
}
is wrong even if no errors can be thrown. By contrast, this code is correct, regardless of whether any of these functions can throw: void foo() {
try {
var res = acquireResource();
useResource(res);
} finally {
releaseResource(res);
}
}
Similarly, in Go this function is wrong: func foo() error {
res, err := acquireResource()
if err != nil {
return fmt.Errorf("error acquiring resources while foo-ing: %w", err)
}
err = useResource(res)
if err != nil {
err = fmt.Errorf("error using resource while foo-ing: %w", err)
}
releaseErr := releaseResource(res)
if releaseErr != nil {
err = fmt.Errorf("%w; failed to releasing resource while foo-ing: %w", err, errW)
}
return err
}
even though I don't have any exceptions.The correct way to do this in Go would be:
func foo() error {
res, err := acquireResource()
if err != nil {
return fmt.Errorf("error acquiring resources while foo-ing: %w", err)
}
defer func() {
err := releaseResource(res)
if releaseErr != nil {
logInSomeWay(fmt.Errorf("failed to releasing resource while foo-ing: %w", err))
}
}()
err = useResource(res)
if err != nil {
return fmt.Errorf("error using resource while foo-ing: %w", err)
}
}
So overall, whether a function finishes early SHOULD be irrelevant.If you want to see what throws "FooException", you need an IDE to do it, unless you have the whole Javadoc memorised. It might be a hundred lines up and in the middle of a foo().bar().baz() chained series of method calls.
And yes, you need to know. The same exception in two different places might mean two very different things in terms of what actually broke, or what you should do to recover or fail the task at hand. Because of this, bubbling up raw exceptions out of deeply nested functions is generally a code smell, because by the time they get to the top level of even a module, all you know is "it broke". Unless you did the donkey work of wrapping the raw exception in something with more semantic context. The same work you'd do in Go.
This isn't any different in Go. Tracing Exceptions is actually often easier since they have unique names that you can grep for, whereas err is always err, so your only recourse is to trace across the callstack.
It's, like, actually worse.
> Because of this, bubbling up raw exceptions out of deeply nested functions is generally a code smell, because by the time they get to the top level of even a module, all you know is "it broke". Unless you did the donkey work of wrapping the raw exception in something with more semantic context.
What? If you have a decent root level error message, that + a stack trace is almost always enough, and is as good or better than what you get in go, since the errors are more structured. I'd generally consider the donkey work you're describing an antipattern. Adding extra context or re-raising an exception should be done only rarely, when you have confidence that the new context you're providing is more useful than the prior (and lots of good languages support exception chaining so you get all of the context from many errors, instead of just one string).
That's a completely different problem, and one that is not unique to errors or exceptions. For any polymorphic type where you want to handle different variants differently, you need to know the possible variants, and this information is, by definition, not present in the code sending the polymorphic value. This is true whether you want to handle exceptions in C++, Optional t in Haskell, or interface values in Go.
> Because of this, bubbling up raw exceptions out of deeply nested functions is generally a code smell, because by the time they get to the top level of even a module, all you know is "it broke". Unless you did the donkey work of wrapping the raw exception in something with more semantic context.
Wrapping up exceptions at every function is a huge code smell. You sometimes want some level of wrapping, especially around module boundaries or in functions with highly relevant context along the way, but generally most functions along a call stack don't have anything to add to a lower level exception - especially if the lower-level library is well made. For example, fs.PathError in Go and System.IO.FileNotFoundException in C# include a way to programatically find the name of the file that was not found, so even if thrown from a low level, a higher level still has the most relevant context available without wrapping. In contrast, java.io.FileNotFoundException does not include this information unless you parse the error string, so you would have a reason to wrap it with more relevant context.