Go in 2 Weeks #05

Key takeaways

Learn explicit Go error handling instead of C++ try/catch: multiple return values, the if err != nil pattern, defer for cleanup, and correct use of panic/recover—with practical examples.

Series overview

📚 Go in 2 Weeks #05 | Full series index

This post covers Days 8–9 of the two-week Go curriculum for C++ developers.

Previous: #04 Interfaces ← | → Next: #06 Goroutines & channels


Introduction: the philosophy of explicit errors

In C++, try-catch lets you handle errors in one place. That is convenient, but it is often hard to see where an exception might be thrown: any function call in a try block, including an overloaded operator or a constructor, might be the one that throws, and the signature doesn’t tell you. Go does not use exceptions for ordinary failures. Instead, errors are returned as values, and callers handle them explicitly at each call site. It feels verbose at first, but every point where a function can fail is visible in the source, and code review can ask “what happens if this fails?” line by line.

The trade-off is real in both directions. Exceptions keep the happy path uncluttered and make it impossible to silently ignore a failure: an unhandled exception terminates the program. Go’s error values make the failure path explicit, but they can be ignored, either by assigning to _ or by simply not checking the second return value of a call like f.Close(). Linters such as errcheck (bundled in golangci-lint) exist precisely to catch that. What you get in exchange is that there is no hidden unwinding: when a Go function returns, it returns through the code you can see, which makes resource cleanup and partial-failure reasoning simpler than exception-safety guarantees in C++.

This post covers multiple return values, the if err != nil pattern, defer as Go’s answer to RAII, error wrapping with %w and errors.Is / errors.As, and where panic and recover fit. The comparisons are written for readers coming from C++.


Errors as values: the error type

C++ vs Go: how errors are handled

// C++: errors via exceptions
#include <iostream>
#include <fstream>
#include <stdexcept>
void readFile(const std::string& path) {
    std::ifstream file(path);
    if (!file) {
        throw std::runtime_error("Failed to open file");
    }
    
    std::string line;
    while (std::getline(file, line)) {
        if (line.empty()) {
            throw std::invalid_argument("Empty line found");
        }
        std::cout << line << "\n";
    }
}
int main() {
    try {
        readFile("data.txt");
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }
    return 0;
}
// Go: errors via return values
package main
import (
    "bufio"
    "fmt"
    "os"
)
func readFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("failed to open file: %w", err)
    }
    defer file.Close()
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        if line == "" {
            return fmt.Errorf("empty line found")
        }
        fmt.Println(line)
    }
    
    if err := scanner.Err(); err != nil {
        return fmt.Errorf("scan error: %w", err)
    }
    
    return nil
}
func main() {
    if err := readFile("data.txt"); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

Key differences:

  • Go does not throw exceptions
  • Errors are passed as return values
  • Callers must check errors

A few details in the Go version are worth reading closely. os.Open returns (*os.File, error); by convention the error is always the last return value, and when it is non-nil the other values should be treated as meaningless. defer file.Close() is placed after the error check, because if Open failed, file is nil and there is nothing to close. The scanner loop is the part people most often get wrong: bufio.Scanner.Scan() returns false both at end of input and on a read error, so the scanner.Err() check after the loop is the only way to tell them apart. Skipping it means a truncated read looks like a successful one.

Also note that bufio.Scanner has a default maximum token size of 64 KiB. A line longer than that stops the loop and scanner.Err() returns bufio.ErrTooLong (“bufio.Scanner: token too long”). If you don’t check Err(), the symptom is a file that is silently processed only up to the first very long line; scanner.Buffer() raises the limit.

The error interface

// Go: error is an interface
type error interface {
    Error() string
}
// Custom error type
type FileError struct {
    Path string
    Op   string
    Err  error
}
func (e *FileError) Error() string {
    return fmt.Sprintf("%s %s: %v", e.Op, e.Path, e.Err)
}
// Usage
func openFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        return &FileError{
            Path: path,
            Op:   "open",
            Err:  err,
        }
    }
    return nil
}

error is just an interface with one method, so any type with an Error() string method is an error. That is how the standard library attaches structured data: *os.PathError (the real type behind the example above) carries Op, Path and the underlying Err, and callers can extract it with errors.As instead of parsing the message string. Implement Error() on the pointer receiver and return &FileError{...} so that errors.As targets are consistently *FileError.

The typed-nil trap

The most confusing bug for newcomers comes from the interface representation. An interface value is nil only when both its dynamic type and its value are nil. If a function’s declared return type is a concrete pointer and you convert it to error, a nil pointer becomes a non-nil interface:

func validate(s string) *FileError {
    if s == "" {
        return &FileError{Op: "validate", Path: s, Err: errors.New("empty")}
    }
    return nil // nil *FileError
}

func run() error {
    return validate("ok") // wraps a nil *FileError in a non-nil error
}

// run() != nil is true, and printing it calls Error() on a nil pointer

The first time you hit this, the symptom is an if err != nil branch that fires even though nothing failed, sometimes followed by a nil-pointer panic inside Error(). The rule that avoids it: functions that can fail should declare error as their return type and return nil literally, never a typed nil pointer.


The if err != nil pattern

Basic pattern

// Go: the most common pattern
func doSomething() error {
    result, err := someOperation()
    if err != nil {
        return err  // propagate
    }
    
    // use result
    return nil
}

Variations

// Go: different ways to handle errors
package main
import (
    "fmt"
    "os"
)
// 1. Return immediately
func pattern1() error {
    f, err := os.Open("file.txt")
    if err != nil {
        return err
    }
    defer f.Close()
    // ...
    return nil
}
// 2. Log then return
func pattern2() error {
    f, err := os.Open("file.txt")
    if err != nil {
        fmt.Println("Failed to open file:", err)
        return err
    }
    defer f.Close()
    // ...
    return nil
}
// 3. Use a default value
func pattern3() int {
    data, err := fetchData()
    if err != nil {
        return 0  // default
    }
    return data
}
// 4. Retry
func pattern4() error {
    maxRetries := 3
    for i := 0; i < maxRetries; i++ {
        err := tryOperation()
        if err == nil {
            return nil
        }
        fmt.Printf("Attempt %d failed: %v\n", i+1, err)
    }
    return fmt.Errorf("failed after %d retries", maxRetries)
}

These four patterns are not equally good, and each has a characteristic failure mode:

  • Return immediately is the default. Returning a bare err without context is fine inside a small package, but at package boundaries it produces messages like open file.txt: no such file or directory with no hint of which operation needed the file. Wrapping (section 4) solves that.
  • Log then return causes duplicated logs. If every layer logs and returns, one failure appears five times in your logs with slightly different text. The usual rule is handle an error once: either log it (and stop propagating), or return it with context, not both.
  • Default value is appropriate only when the default is genuinely acceptable to the caller, such as a missing optional config key. Used carelessly it turns an outage into silently wrong data; if you do it, at least log or count the failure.
  • Retry as written retries immediately, which against an overloaded service makes things worse. Real retry loops add backoff with jitter, check ctx.Done() so a cancelled request stops retrying, and retry only errors that are actually transient (a timeout, not a 404). The final error should also wrap the last underlying error, not just say “failed after 3 retries”.

defer: RAII’s replacement

C++ RAII vs Go defer

// C++: automatic cleanup with RAII
void processFile() {
    std::ifstream file("data.txt");
    std::lock_guard<std::mutex> lock(mtx);
    
    // work...
    
    // On scope exit:
    // 1. lock released (lock_guard destructor)
    // 2. file closed (ifstream destructor)
}
// Go: explicit cleanup with defer
func processFile() error {
    file, err := os.Open("data.txt")
    if err != nil {
        return err
    }
    defer file.Close()  // runs when the function returns
    
    mu.Lock()
    defer mu.Unlock()   // runs when the function returns
    
    // work...
    
    return nil
    // defers run in reverse order:
    // 1. mu.Unlock()
    // 2. file.Close()
}

The key difference is scope. A C++ destructor runs when the block ends; a Go defer runs when the function returns. That matters in loops:

for _, path := range paths {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // not closed until the whole function returns
    // ...
}

With a few thousand paths this exhausts file descriptors and os.Open starts failing with too many open files. The fix is to move the loop body into its own function (or a closure called immediately), so each defer runs at the end of each iteration.

defer also has a small cost, but since Go 1.14 most defers are “open-coded” by the compiler and cost close to a direct call, so avoiding defer for performance is rarely justified anymore.

Defer order (LIFO)

// Go: defer is LIFO (Last In First Out)
package main
import "fmt"
func example() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    
    fmt.Println("function body")
}
func main() {
    example()
}
// Output:
// function body
// 3
// 2
// 1

When defer arguments are evaluated

// Go: defer arguments are evaluated immediately
package main
import "fmt"
func example() {
    x := 1
    defer fmt.Println("Deferred:", x)  // x == 1 is captured now
    
    x = 2
    fmt.Println("Current:", x)
}
func main() {
    example()
}
// Output:
// Current: 2
// Deferred: 1  (value at defer registration)

Deferred evaluation via a closure:

// Go: wrap in a function to observe the value at run time
func example() {
    x := 1
    defer func() {
        fmt.Println("Deferred:", x)  // uses x when the defer runs
    }()
    
    x = 2
    fmt.Println("Current:", x)
}
// Output:
// Current: 2
// Deferred: 2  (value at execution time)

Practical defer patterns

// Go: defer patterns in production
package main
import (
    "fmt"
    "time"
)
// 1. Timing
func measureTime(name string) func() {
    start := time.Now()
    return func() {
        fmt.Printf("%s took %v\n", name, time.Since(start))
    }
}
func slowFunction() {
    defer measureTime("slowFunction")()  // defer the returned function
    
    time.Sleep(100 * time.Millisecond)
}
// 2. Locking
func criticalSection() {
    mu.Lock()
    defer mu.Unlock()
    
    // unlock is guaranteed even with multiple return paths
    if condition1 {
        return
    }
    if condition2 {
        return
    }
    // ...
}
// 3. Transaction rollback
func transaction() (err error) { // named result: the deferred closure sees the final error
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    
    defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()
    
    // work...
    if err = doWork(tx); err != nil { // '=' not ':=' so the named err is set
        return err  // defer runs Rollback
    }
    
    return tx.Commit()
}

The transaction pattern has a classic bug that is easy to write: with an unnamed error result and if err := doWork(tx); ..., the := creates a new, shadowed err, and the deferred closure checks the outer err, which is still nil from db.Begin(). The rollback never runs and the connection is returned to the pool with an open transaction. Using a named result fixes it, because return x assigns to the named err before deferred functions run. A simpler alternative that many codebases prefer is an unconditional defer tx.Rollback(): after a successful Commit, Rollback just returns sql.ErrTxDone, which you ignore.

The same named-result technique is how you surface errors from Close on a file you wrote: defer func() { if cerr := f.Close(); cerr != nil && err == nil { err = cerr } }(). For files opened for reading, ignoring the Close error is fine; for writes, Close can be the first place a delayed write failure (full disk, NFS) is reported.


Wrapping and unwrapping errors

Error wrapping

// Go: add context by wrapping
package main
import (
    "fmt"
    "os"
)
func readConfig(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        // %w preserves the underlying error
        return fmt.Errorf("read config from %s: %w", path, err)
    }
    
    if len(data) == 0 {
        return fmt.Errorf("config file %s is empty", path)
    }
    
    return nil
}
func loadSettings() error {
    if err := readConfig("config.json"); err != nil {
        return fmt.Errorf("load settings: %w", err)
    }
    return nil
}
func main() {
    if err := loadSettings(); err != nil {
        fmt.Println("Error:", err)
        // Error: load settings: read config from config.json: open config.json: no such file or directory
    }
}

errors.Is and errors.As

// Go: test for a specific error
package main
import (
    "errors"
    "fmt"
)
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)
func findUser(id int) error {
    if id < 0 {
        return ErrNotFound
    }
    return nil
}
func main() {
    err := findUser(-1)
    
    if errors.Is(err, ErrNotFound) {
        fmt.Println("User not found")
    }
}
// Go: extract a concrete type with errors.As
package main
import (
    "errors"
    "fmt"
    "os"
)
func openFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("open file: %w", err)
    }
    return nil
}
func main() {
    err := openFile("nonexistent.txt")
    
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Println("Path error:")
        fmt.Println("  Op:", pathErr.Op)
        fmt.Println("  Path:", pathErr.Path)
        fmt.Println("  Err:", pathErr.Err)
    }
}

With fmt.Errorf and %w, you build an error chain, so even after several layers you can still use errors.Is(err, os.ErrNotExist) to find the root cause. With %v only, you concatenate strings and Is / As cannot walk the chain.

errors.Is compares each error in the chain with == (or the error’s own Is method), so it works for sentinel values. errors.As walks the same chain looking for an error assignable to the target’s type, and it needs a pointer to a variable of that type: errors.As(err, &pathErr). Passing pathErr itself panics with errors: target must be a non-nil pointer, and passing a pointer to a type that doesn’t implement error panics as well; go vet catches most of these at build time.

Wrapping is a design decision, not just formatting. Once you wrap with %w, callers can depend on the underlying error, so it becomes part of your package’s API: if you later switch from a SQL database to an HTTP backend, code that checked errors.Is(err, sql.ErrNoRows) breaks. When the underlying error is an implementation detail, either translate it into your own sentinel (return ErrNotFound) or format it with %v to deliberately hide it. Since Go 1.20, fmt.Errorf accepts multiple %w verbs and errors.Join combines several errors into one, which is handy when, for example, cleanup fails after the main operation already failed.


errors.New vs fmt.Errorf, sentinels, real-world wrapping

errors.New vs fmt.Errorf

APIWhen to useNotes
errors.New("msg")Simple errors with fixed messages, often sentinel package-level vars compared with errors.IsNo formatting—single string only
fmt.Errorf("format", args...)When you need dynamic values in the messageFormatting alone is not wrapping
fmt.Errorf("...: %w", err)When you want the cause on the chain (Go 1.13+)Unwrap works → errors.Is / As can traverse
package example
import (
    "errors"
    "fmt"
)
var ErrNotFound = errors.New("not found") // Sentinel — compare with Is
func badUser(id int) error {
    return fmt.Errorf("user %d not found", id) // OK, but use %w to link ErrNotFound
}
func goodUser(id int) error {
    return fmt.Errorf("user %d: %w", id, ErrNotFound) // chain includes ErrNotFound
}

Custom errors and errors.As

  • Use errors.Is to compare values (sentinels or the Unwrap chain).
  • Use errors.As to extract a concrete type and read fields (HTTP status, error codes, etc.).
  • Custom types need only Error() string; add Unwrap() error when you wrap another error.
package example
import "fmt"
type AppError struct {
    Code    int
    Message string
    Err     error
}
func (e *AppError) Error() string {
    return fmt.Sprintf("code=%d: %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error { return e.Err }

Sentinel errors in practice

  • Declare var ErrXxx = errors.New(...) at package scope to fix meaning.
  • Callers use errors.Is(err, pkg.ErrNotFound); intermediate layers add context with fmt.Errorf("context: %w", err).
  • Creating multiple errors.New with the same string breaks Is. Always use shared variables.

Wrapping checklist

  1. Wrap at failure sites: return fmt.Errorf("doing X: %w", err).
  2. Classify with errors.Is / errors.As (string Contains is a last resort).
  3. Logging: agree when full stacks are needed—usually once at the boundary.

panic and recover

panic: unrecoverable mistakes

// C++: throw an exception
void divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("division by zero");
    }
    std::cout << a / b << "\n";
}
// Go: panic (generally discouraged for normal flow)
func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")  // unwinds the goroutine; unrecovered, it crashes the whole program
    }
    return a / b
}
// Preferred: return an error
func divideWithError(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

recover: catching a panic

// Go: recover from panic
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()
    
    result = a / b  // panics if b == 0
    return result, nil
}
func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

When to use panic / recover

SituationRecommendation
Ordinary failures (missing file, network timeout, bad input)Return error
Programming bugs (broken invariants, nil dereference, unreachable branch)May panic—prefer tests to catch these
Must-succeed initialization (regexp.MustCompile, template.Must)Idiomatic panic
Isolate one HTTP requestrecover only makes sense at goroutine boundaries—middleware / handler patterns (ties to #06

recover works only inside a deferred function, and only when called directly by that deferred function; calling it from a helper that the deferred function calls returns nil. It also only catches panics in its own goroutine. This is the point that surprises C++ developers most: if a goroutine you started with go worker() panics, no recover in main or in the code that launched it can catch it, and the whole process exits with panic: ... followed by the goroutine’s stack trace. Any long-lived goroutine that might panic needs its own defer/recover at the top of its function.

net/http already does this for handlers: a panicking handler is recovered by the server, which logs http: panic serving <addr>: ... and closes that connection, while other requests continue. That is convenient, but it also means a handler bug may show up only as dropped connections and log lines rather than a crash. Prefer logging and converting to error (or returning 500) rather than swallowing silently. Note that some failures are not panics at all: a concurrent map write is reported as fatal error: concurrent map writes, and fatal errors cannot be recovered.

Panic guidelines

// Bad: panic for ordinary errors
func fetchUser(id int) *User {
    user, err := db.Query(id)
    if err != nil {
        panic(err)  // don't do this
    }
    return user
}
// Good: return an error
func fetchUser(id int) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("fetch user %d: %w", id, err)
    }
    return user, nil
}
// OK: panic for programmer mistakes
func mustCompile(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic(fmt.Sprintf("invalid regex pattern: %s", pattern))
    }
    return re
}
// Startup-time invariant
var emailRegex = mustCompile(`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`)

Exercises

Exercise 1: copy a file with defer

// Go: safe file copy with defer
package main
import (
    "fmt"
    "io"
    "os"
)
func copyFile(src, dst string) error {
    srcFile, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("open source: %w", err)
    }
    defer srcFile.Close()  // always closed
    
    dstFile, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("create destination: %w", err)
    }
    defer dstFile.Close()  // always closed
    
    _, err = io.Copy(dstFile, srcFile)
    if err != nil {
        return fmt.Errorf("copy: %w", err)
    }
    
    return nil
}
func main() {
    if err := copyFile("source.txt", "dest.txt"); err != nil {
        fmt.Fprintf(os.Stderr, "Copy failed: %v\n", err)
        os.Exit(1)
    }
    fmt.Println("Copy successful")
}

Exercise 2: custom error type

// Go: custom error type
package main
import (
    "errors"
    "fmt"
    "strings"
)
type ValidationError struct {
    Field   string
    Value   string
    Message string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error [%s=%s]: %s", 
        e.Field, e.Value, e.Message)
}
func validateEmail(email string) error {
    if email == "" {
        return &ValidationError{
            Field:   "email",
            Value:   email,
            Message: "email is required",
        }
    }
    
    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Value:   email,
            Message: "invalid email format",
        }
    }
    
    return nil
}
func registerUser(email string) error {
    if err := validateEmail(email); err != nil {
        return fmt.Errorf("register user: %w", err)
    }
    
    // registration logic...
    return nil
}
func main() {
    err := registerUser("invalid")
    
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        fmt.Println("Validation failed:")
        fmt.Println("  Field:", validationErr.Field)
        fmt.Println("  Value:", validationErr.Value)
        fmt.Println("  Message:", validationErr.Message)
    }
}

Exercise 3: error chains

// Go: tracing an error chain
package main
import (
    "errors"
    "fmt"
)
var (
    ErrDatabase   = errors.New("database error")
    ErrConnection = errors.New("connection error")
)
func connectDB() error {
    return ErrConnection
}
func queryUser(id int) error {
    if err := connectDB(); err != nil {
        return fmt.Errorf("query user %d: %w", id, err)
    }
    return nil
}
func getProfile(id int) error {
    if err := queryUser(id); err != nil {
        return fmt.Errorf("get profile: %w", err)
    }
    return nil
}
func main() {
    err := getProfile(123)
    
    fmt.Println("Error:", err)
    // Error: get profile: query user 123: connection error
    
    if errors.Is(err, ErrConnection) {
        fmt.Println("Root cause: connection error")
    }
}

Exercise 4: timing with defer

// Go: measure execution time with defer
package main
import (
    "fmt"
    "time"
)
func trace(name string) func() {
    start := time.Now()
    fmt.Printf("Entering %s\n", name)
    
    return func() {
        fmt.Printf("Exiting %s (took %v)\n", name, time.Since(start))
    }
}
func processData() {
    defer trace("processData")()
    
    fmt.Println("Processing...")
    time.Sleep(100 * time.Millisecond)
    fmt.Println("Done")
}
func main() {
    processData()
}
// Output:
// Entering processData
// Processing...
// Done
// Exiting processData (took ~100ms, e.g. 100.4ms)

Note the trailing () in defer trace("processData")(). trace(...) runs immediately when the defer statement executes (printing “Entering” and capturing the start time), and the function it returns is what gets deferred. Forgetting the second () compiles to deferring trace itself, so both messages print at the end and the measured duration is near zero.


Wrap-up: Days 8–9 checklist

What to complete

  • Understand the error type and if err != nil
  • Pass errors with multiple return values
  • Use defer for cleanup (RAII-style guarantees)
  • Know defer runs in LIFO order
  • Wrap with fmt.Errorf and %w
  • Distinguish errors.New (sentinels) from fmt.Errorf / %w
  • Inspect errors with errors.Is and errors.As
  • Use panic / recover sparingly
  • Finish all four exercises

From C++ to Go

C++GoNotes
throwreturn errorExplicit
try-catchif err != nilPer call site
RAIIdeferPer function
Exception propagationError return chainClear flow
std::exceptionerror interfaceMinimal surface

What’s next

In Days 8–9 you learned Go error handling. Next: goroutines and channels—where many C++ developers really feel Go’s strength.


Series navigation

PreviousIndexNext
← #04 Interfaces📑 Full index#06 Goroutines & channels →

Go in 2 Weeks: Curriculum • #01 Syntax & philosophy • #02 Data structures • #03 OOP & composition • #04 Interfaces • #05 Error handling • #06 Goroutines & channels • #07 Modules & testing • #08 REST API • #09 Context & graceful shutdown


Go favors explicit error returns for clear control flow, and defer for reliable cleanup.

FAQ

Q. When does defer run?

A. When the surrounding function returns (including via panic). Multiple defers run in LIFO order: the last registered runs first. Note that the arguments of a deferred call are evaluated when the defer statement runs, not when the call executes.

Q. When should I use panic?

A. For unrecoverable programming bugs (for example a nil dereference or index out of range). Ordinary failures such as a missing file or a bad request should return an error value.