5 ms·
The fact that error handling is "at the forefront of your mind" is exactly the problem. Instead of following the code, you have to constantly deal with the irre
by simiones 5y ago
The fact that error handling is "at the forefront of your mind" is exactly the problem. Instead of following the code, you have to constantly deal with the irrelevant detail of error wrapping. 99.9% of code doesn't handle errors - it returns them further up the call stack (in rare cases with some extra context). This is exactly what exceptions do, without someone having to constantly write code for them to do so.
- deleted 5y ago[deleted]
- thiht 5y agoThe fact it’s explicit is a good thing though. Exceptions are a nightmare to handle and a worse mental overhead when dealing with code because you have to keep the possible exceptions in your mind.
- JulianMorrison 5y agoOr use an IDE that parses the language and helps you not notice that important parts of your program's behaviour are not visible from its text.
- simiones 5y agoBut that's just it: when writing code the right way, there are no important parts of the code's behavior that are not visible from it's text. 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.
- JulianMorrison 5y agoThat... is a very long winded way to miss the point. 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.
- joshuamorton 5y ago> 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. 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).
- simiones 5y ago> If you want to see what throws "FooException" 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.
- jesseduffield 5y agoYou can still be explicit without being verbose, as we've seen with rust's `?` operator
- JulianMorrison 5y agoThere have been attempts at making a Go syntax to do that. So far, not successfully [1]. 1. https://www.infoq.com/news/2019/07/go-try-proposal-rejected/ https://www.infoq.com/news/2019/07/go-try-proposal-rejected/
- simiones 5y agoYou don't need to keep possible exceptions in your mind to any more or less extent in Java than in Go. 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?
- JulianMorrison 5y agoWhen I'm writing Java, I have to keep mental track of the fact that nearly anything might invisibly "return" from in the middle of a function. Checked exceptions help keep a handle on that, but in general they still usually need to be caught and rewritten into something that doesn't leak encapsulation-breaking implementation details. In that that regard, it's the same boilerplate as Go - if, usually, unhelpfully far away from the code that might throw the exception. There is a reason writing Java really needs an IDE. Just looking at the text doesn't tell you "what might throw this?" When I'm writing Go, it returns where it says "return".
- simiones 5y agoWhen reading Java code you only need to pay attention to whether any resource that is acquired is also safely released - i.e. that no resources are acquired without using `using` or `try/finally`. This is the same as Go with `defer`. Beyond that, you don't need to care if the function will terminate early because of an exception - as long as all necessary cleanup is handled regardless of exit reason, you don't care that the function will finish early if an HTTP request fails before using the value of the request. And usually you only want to catch and re-throw exceptions around module (in the broad sense) boundaries. If you catch and re-throw exceptions in every function, you're either losing lower-level context completely OR you're leaking implementation details anyway if you're chaining up the old error. In Go, the error needs to be manually propagated through every function, with enough context to understand what's going on. In Java, the error needs to be manually handled at every module boundary; function-level context is given automatically by the call-stack.
- zaphar 5y agoJava doesn't require you to handle exceptions. This is not the criteria you should use to determine if you need handle the exception. The fact is that even code that does no resource acquiring could throw an exception. It might not even be a documented exception. And when it throws that exception that you may or may not have been expecting some code somewhere is going to have to figure out what to do. In java the only real way to communicate an error is to throw an exception. For some reason culturally Java has decided that it's too much work to distinguish between errors that should crash the app and errors that should be handled by the caller in some way. As a result when an exception outside of your code happens in production and you read the exception in your logs it will: 1. Not have a significant part of the information you need to debug the problem. 2. Be in code you may not have easy access to go read. 3. You will probably have no idea why it's happening. Eventually for a long enough lived codebase you will have encountered and documented all of these in a runbook somewhere or you won't and everyone will have to have learned them over and over again. If you are on an exceptional team you'll have caught and then wrapped or handled these exceptions to make operating the software in production less painful. But all of it could have been avoided if java had chosen a better path. Make it impossible trap runtime exceptions. They just crash the program. Make trappable exceptions checked exceptions. The code would get more verbose. But the operational pain would have been vastly reduced. Errors are a part of your API. Unchecked Runtime exceptions pretend like 50% of your API surface doesn't exist.
- sagichmal 5y agoError handling is equally important to business logic. This is a core tenet of Go, an explicit design decision. If you don't agree with that statement, then you're gonna have a bad time.