Errors are values
Go has no exceptions and no try/catch. A function that can fail returns an error as its last result, and the caller checks it right away with if err != nil. An error is an ordinary value: you can store it, return it, compare it, put it in a slice or print it.
error is a built-in interface with one method, Error() string (Module 06 showed how interfaces work). The two quickest ways to make one are errors.New("message") for a fixed message and fmt.Errorf("format", args...) when the message needs values in it. nil means "no error".
package main
import (
"errors"
"fmt"
"strconv"
)
func parseAge(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("age %q is not a number", s)
}
if n < 0 || n > 150 {
return 0, errors.New("age out of range")
}
return n, nil
}
func main() {
for _, in := range []string{"42", "abc", "-3"} {
age, err := parseAge(in)
if err != nil {
fmt.Println("error:", err)
continue
}
fmt.Println("age:", age)
}
}age: 42
error: age "abc" is not a number
error: age out of rangeAdd a check that rejects an empty string with its own message, before calling strconv.Atoi.
start server: read config: open app.yaml: no such file or directory. go vet and linters such as staticcheck flag capitalised error strings.The "happy path" stays at the left margin: handle the error, return or continue, and carry on un-indented. This is the same return-early style from Module 02. It looks repetitive at first, but every place a call can fail is visible in the code, and reviewers can see that each failure was thought about.
declared and not used: err
package main
import (
"fmt"
"strconv"
)
func main() {
n, err := strconv.Atoi("42")
fmt.Println(n)
}# command-line-arguments
./main.go:9:5: declared and not used: errGo refuses to compile a local variable that is never read. With errors that is a feature: receiving an error and then forgetting to check it is caught at compile time.
Check the error. If you truly do not care (rare), discard it explicitly with _ so the decision is visible.
package main
import (
"fmt"
"strconv"
)
func main() {
n, err := strconv.Atoi("42")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(n)
}too many return values
package main
import (
"fmt"
"os"
)
func loadConfig(path string) error {
_, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return nil
}
func main() {
fmt.Println(loadConfig("app.yaml"))
}# command-line-arguments
./main.go:11:15: too many return values
have (nil, error)
want (error)loadConfig is declared to return only error, but the early return passes two values — a habit carried over from functions that return (T, error). The compiler lists what you returned and what the signature wants.
Return exactly what the signature declares: here just err (better still, wrapped with context — next lesson).
package main
import (
"fmt"
"os"
)
func loadConfig(path string) error {
_, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
return nil
}
func main() {
fmt.Println(loadConfig("app.yaml"))
}