I don't think the article's `CopyFile` function is a good example for either `guard` statements or exceptions.
We can come up with better examples (feel free), but I'm going to focus on that since it's the one used in the article.
As a library consumer, I would like to know if the error happened in the source file or the destination file, ideally without requiring me to know implementation details of the function, so I don't have to worry if the implementation changes and now my `if errors.Is(err, fs.PathError)` no longer works and instead became a bug.
For example, the caller might want to:
* Show a user-friendly message to the user if the problem is in the source file.
* Return gracefully if there's not enough space to create the destination file (e.g. "copy as many files as you can to this drive, and tell me which ones you copied").
* "Bubble-up" the error in any other situation.
A function that lets me know which file was the cause of the problem might look like this:
type ErrSourceFile struct{ error }
type ErrDestFile struct{ error }
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return &ErrSourceFile{error: err}
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return &ErrDestFile{error: err}
}
defer w.Close()
if _, err := io.Copy(w, r); err != nil {
return err
}
if err := w.Close(); err != nil {
return &ErrDestFile{error: err}
}
return nil
}
Since now errors are actually useful to the caller function, the `guard` statements became unnecessary everywhere except in the `guard io.Copy(w, r)` case, because now we must handle nils (though I guess with generics we can create a wrapper that allows doing stuff like `guard WrapIfNonNil[ErrSourceFile](err)`, but whether that's better is debatable).If we tried to do this with exceptions so that we can actually catch them, the code would become:
type SourceFileException exception
type DestFileException exception
func CopyFile(src, dst string) throws error {
// Now necessary because the `try` blocks introduce new scopes.
var (
r io.ReadCloser
w io.WriteCloser
)
try {
r = os.Open(src)
} catch err {
// Wrap exception because we don't want to hide
// the inner exception.
throw SourceFileException(err)
}
defer r.Close()
try {
w = os.Create(dst)
} catch err {
throw DestFileException(err)
}
defer w.Close()
io.Copy(w, r)
// The caller might want to check the hash to decide if it
// should delete the file or not.
try {
w.Close()
} catch err {
throw DestFileException(err)
}
}
... which is even more cumbersome than plain `if` blocks.Exceptions have their advantages in certain cases, but they also have their downsides, it all depends on the situation and what you want to do.
As everything, it's a trade-off.