Start learning free

LESSON 145 OF 173 · BACKEND: TESTS AND RUNNING IT

When Something Breaks in Go

Practice

Every shop has bugs nobody has found yet. In this shop the Gift card (product 8) has no category: it is NULL in the database, and a Go string can't hold NULL, so Scan returns an error and the route calls panic(err). When a route panics, Go's net/http logs it with a stack trace and closes the connection: the visitor gets no answer at all, not even a status code.

You rescue a panic with a deferred function. A deferred call runs when its function ends, also when it ends in a panic, and recover() inside it stops the panic and gives its value, or nil when nothing panicked. Here it is in a small program, away from any route:

func main() {
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("stopped a panic:", err)
        }
    }()
    panic("something broke")
}

It prints stopped a panic: something broke instead of crashing. A 500 answer should never carry the error's own text: it names your columns, tables and files, gives an attacker a map of your code, and means nothing to a customer. Send a short message instead, and write the details to the log with log.Printf, where only you read them. A function you defer from the route can take w and r, so it can answer and say which request failed.

The check sends three requests, closes the database for a fourth, and runs your test on the shop and on two broken copies. It must print:

The shop gets three requests:
GET /products/1 -> 200 {"category": "Kitchen", "name": "Mug"}
GET /products/99 -> 404 {"error": "Product not found"}
GET /products/8 -> 500 {"error": "Something went wrong on our side"}
  log: GET /products/8 failed: sql: Scan error on column index 1, name "category": converting NULL to string is unsupported
The database closes:
GET /products/1 -> 500 {"error": "Something went wrong on our side"}
  log: GET /products/1 failed: sql: database is closed
Your tests on the shop:
  TestCrashAnswersJSON ... ok
  1 test: OK
Your tests on a copy without serverError, where net/http closes the connection:
  TestCrashAnswersJSON ... ERROR
    panic: runtime error: invalid memory address or nil pointer dereference
  1 test: 1 failed, so they caught this bug
Your tests on a copy that sends the error to the visitor:
  TestCrashAnswersJSON ... FAIL
    Not equal: expected map[error:Something went wrong on our side], actual map[error:sql: Scan error on column index 1, name "category": converting NULL to string is unsupported]
  1 test: 1 failed, so they caught this bug

Your task: below newApp, write serverError(w http.ResponseWriter, r *http.Request). It calls recover(), and when that gives an error, it logs one line with log.Printf and the text "%s %s failed: %v", filled with the method, the path and the error, then answers {"error": "Something went wrong on our side"} with writeJSON and the status 500. Defer a call to it as the first line of the route's function. Then, in ErrorTests, write TestCrashAnswersJSON: get /products/8 and check both the status code and the JSON body.


Assignment
Follow the Coding Tutorial to add serverError with its log line, defer it in the route, then write a test that checks both the status and the JSON body of a crash.


Hint
recover() only stops a panic inside a deferred call, so serverError does nothing until the route says defer serverError(w, r). A test that checks only the status code catches the copy that gives no answer, but lets the leaky copy through, because that is a 500 too: compare readJSON(response) with the map as well.


STEP 1 OF 10
    Ctrl + Enter to check

    Ops 500 Handler in Go

    Keep going with Go1,600+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free