4 ms·
Current state of the art: func read_file(filename string) (string, error) { return "", errors.New("oops") } func foo() error { a, err := read_fi
by linkdd 5y ago
Current state of the art:
func read_file(filename string) (string, error) {
return "", errors.New("oops")
}
func foo() error {
a, err := read_file("a.txt")
if err != nil {
return errors.New(fmt.Sprintf("read a: %s", err))
}
b, err := read_file("b.txt")
if err != nil {
return errors.New(fmt.Sprintf("read b: %s", err))
}
// do stuff with a and b
return nil
}
func main() {
err := foo()
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
In a language with exceptions:
func read_file(filename string) string {
throw FileNotFound(filename)
}
func foo() {
a := read_file("a.txt")
b := read_file("b.txt")
// do stuff with a and b
}
func main() {
try {
foo()
}
catch (err FileNotFound) {
fmt.Fprintf(os.Stderr, "file not found: %s\n%s\n", err.filename, err.Stacktrace())
}
}
With the current go error handling, you need to add the informations yourself in the string, not as a real data structure.
And before you say "you can add the filename to the error message in read_file()", what if the function is defined in a dependency you have no control over?
An exception is a typed data structure that contains way more informations and value to automate rescuing.
Delegating error handling to a try/catch block with a typed data structure allows the caller to care for certain type of errors and delegate the others to its own caller. With the current error type in Go, what would you do? parse the error message?
- randomdata 5y agoYour code is wrong. It would normally be written something like this: func read_file(filename string) string { panic(FileNotFound{filename}) } func foo() { a := read_file("a.txt") b := read_file("b.txt") // do stuff with a and b } func main() { defer func() { if err, ok := recover().(FileNotFound); ok { fmt.Fprintf(os.Stderr, "file not found: %s\n%s\n", err.filename, err.Stacktrace()) } } foo() } However, exceptions are meant for exceptional circumstances (hence the name), not errors. A file error is not exceptional in the slightest. It is very much expected. While you can overload exceptions to pass errors (or any other value), that does not mean you should. Your use of exceptions for flow control (i.e. goto) is considered harmful.
- linkdd 5y agoOk I was wrongly assuming that panic was expecting an error type, in fact it's an interface{}. > Your use of exceptions for flow control (i.e. goto) is considered harmful Exceptions are a way to delegate error handling to the caller by giving them informations about the unexpected behavior. It implies that the expected behavior is the "happy path" (everything went well) and any deviations (errors) is unexpected. This is far from a goto because you can have `try/finally` blocks without catch (or defer in golang). Also, exceptions are just a kind of algebraic effects that do not resume. There was a proposal to JS for this: https://github.com/macabeus/js-proposal-algebraic-effects https://github.com/macabeus/js-proposal-algebraic-effects This is also easier to test. assertRaises(ErrorType, func() { code... }) Almost every Go library I've seen just return an error (which is just a string), you'd need to parse it to assert that the correct error is returned in special conditions.
- randomdata 5y ago> It implies that the expected behavior is the "happy path" (everything went well) and any deviations (errors) is unexpected. Errors are the "happy path", though. Your network connection was lost for the data you were trying to transmit so you saved it to your hard drive instead means that everything went well! Throwing your hands up in the air and crashing your program because you had to make a decision is not something you would normally want to do. If statements are present in most happy paths for good reason. That the inputs presented you with a choice does not remove you from the happy path. Now, if you made a programming mistake and tried to access an array index that is out of bounds, then there isn't much else you can do but crash. Exceptions are appropriate for that kind of problem. They are exceptional in that they should never happen. Errors, on the other hand, are expected to happen and you can happily deal with them.
- linkdd 5y ago> Throwing your hands up in the air and crashing your program because you had to make a decision is not something you would normally want to do And that's not something you do thanks to try/catch. You just handle the error where it's meaningful to handle it. The happy path of "make a request" is that there is no network error. The happy path of "make sure this request is sent" is that you handle the unexpected network error to save the request to disk for further retry. If the disk is full, you're not on the happy path anymore. In Erlang/Elixir, there is a philosophy of "let it crash" which is basically "delegate the error handling/recovery to where it's meaningful to do so". For example: start a supervised process to send a request if there is an unexpected network failure, let it crash the supervisor retries on its own Or: start a process to send a request if there is an unexpected network failure, let it crash monitor the process to be notified when it crash do something if it happens, like saving the request to disk A "write_file" function can return many kind of errors: - file not found (some folder in the path does not exist) - permission denied - disk full When you call write_file, you might want to handle some of those errors, and delegate the handling of others to your caller. You're still not addressing my main point. How do you check which errors you want to handle and which one you want to propagate with just a string describing your error ?
- tptacek 5y agoThis is definitely not the current state of the art. Errors are values with types just like anything else in the language; dynamically populating calls to errors.New() is super weird (the point of errors.New is mostly to create global errors you can compare against), your errors nest without using %w, and there are a bunch of simple libraries that wrap typed stack traces in errors, just like there are a bunch of simple libraries in Rust that exist to make error types compatible with each other.
- deleted 5y ago[deleted]
- benburwell 5y agoYour "state of the art" Go code is not really a good example of how that functionality would be written. If you want to check for a specific error condition, then just define a value for that error and use `errors.Is` to check for it. This works as you'd expect with wrapping: https://go.dev/play/p/rJIlKKSYn9Q https://go.dev/play/p/rJIlKKSYn9Q > With the current go error handling, you need to add the informations yourself in the string, not as a real data structure. This is completely false! If you want to provide a structured error, then you just need to define a type for it. In your example, a Go programmer might use errors.Is(err, fs.ErrNotExist) and errors.As if they wanted to retrieve the specific file path that does not exist in a strongly-typed way, something like https://go.dev/play/p/hdHPLAVbQuW https://go.dev/play/p/hdHPLAVbQuW. > Delegating error handling to a try/catch block with a typed data structure allows the caller to care for certain type of errors and delegate the others to its own caller. With the current error type in Go, what would you do? parse the error message? Certainly not! I think there is a misconception that "an error is a string" -- in Go, an error is actually any type that satisfies the error interface, i.e. has an `Error() string` method. It can be any type at all, and have as many other methods as you like in order to provide the functionality you need. > what if the function is defined in a dependency you have no control over? There's nothing stopping you from writing `throw new Exception(String.format("file not found: %s", filename))` in languages with exceptions either. In both cases, it would be recognized as poor API design. Regarding stack traces, Go makes a strong distinction between errors (generally a deviation from the happy path) and panics (a true programming error, e.g. nil pointer dereference, where the program must exit). Errors do not provide stack traces since there is no need for them in a flow control context, panics do provide stack traces for useful debugging information.