Go's way of error handling is an interesting one. Many know about the typical exceptions used in languages like Java, C# or Python, but go decided to handle errors as values, which leads to several advantages to the classic exception way.
Exceptions handle "exceptional" conditions which disrupt the normal flow of the program. When an error occurs, an exception is "thrown", which immediately stops the current function and unwinds the stack, until the exception is caught. If the exception is not caught, it can go all the way to the main function, which will cause the program to crash if it is not caught there. This approach entangles the error itself with the control flow, which leads to incomprehensible stack traces, with much information about the structure of the program, but little context about what went wrong. This makes the control flow harder to understand, because an exception can be thrown at many places and it might not be clear where it gets caught.
Go tries to improve this by treating errors like values. Errors get handled explicitly by returning an error to the calling function, which puts the responsibility on the calling function. The calling code then checks the error and decides what to do with it.
This might look like this:
file, err := os.Open("file.txt")
if err != nil {
// handle the error
log.Fatal(err)
return
}
// use the file
This needs more attention by the programmer to the error-handling logic, but that is precisely the point.
By convention an error is the last element returned from a function. If it is `nil` then there was no error, otherwise the error value will hold an `error` object that contains information about what went wrong. By using this extra information the calling function may decide what to do with it.
There are several ways to handle an error:
Retry the Operation: If an operation fails due to unpredictable problems, one can retry the failed operation, possibly with a delay or a limit on the number of attempts or the time spent trying.
Stop the Program Gracefully: If progress is impossible, one can stop the program gracefully by printing the error and stopping the program. However, this should be reserved for the main package of a program.
Log the Error and Continue: In some cases, it’s sufficient just to log the error and then continue, perhaps with reduced functionality.
Ignore the Error: In rare cases, one can safely ignore an error entirely. For example, if an operation failure does not affect the rest of the program and handling it is not necessary.
In Go, you always check for errors right after the call that could cause them. If the error causes the function to return, the logic for success follows at the outer level, leading to a common structure of initial checks for errors, followed by the main substance of the function, minimally indented. This gives Go code its characteristic "error handling rhythm".
This approach is more verbose than the exception mechanism, but makes it clearer where the error came from and how it is handled.
In summary there are three big advantages to go's approach of error handling:
It is very explicit. If you look at a function you instantly know that it can return an error and the programmer decides how to handle it.
The flow of control is more straightforward than with exceptions. By propagating errors up the stack, the normal flow of control isn't disrupted, as you explicitly decide how to handle the error. With exceptions, the control flow can jump around, which I find harder to reason about.
Errors are composable and can therefore be easily enriched with more information or context to make them more meaningful.
However, not everybody likes this approach, since it does come with some tradeoffs. One potential drawback of Go's error handling is that it is verbose and repetitive. The necessity to check for errors immediately after each operation that might produce one can lead to code that feels cluttered with error handling logic. This can sometimes make it harder to focus on the core logic of a function or method, as it's interspersed with these necessary error handling checks. Moreover, the error handling responsibility placed on the developer means that there's a risk of mishandling or even overlooking errors, especially in more complex codebases.