The Missing Row TrapYou are building a Go microservice and need to fetch a single user profile. You write a clean db.QueryRow() call, chain it with .Scan(), and everything works in your local environment. Then, a user requests an ID like 9999 that doesn't exist, and your logs explode with 500 Internal Server Errors. The culprit is a misunderstood sentinel error.
The specific message appearing in your logs is:
sql: no rows in result set
Unlike Python (where you might get None) or Node.js (which returns undefined), Go treats a missing row as an explicit error state. It is not a bug in your database driver. Instead, it is a deliberate design choice that forces you to decide exactly what happens when a query comes up empty.
Where the Logic BreaksStandard error handling often fails here because it treats every non-nil error as a catastrophic event. Many developers write code like this:
func GetUserEmail(id int) (string, error) {
var email string
// This query looks perfect at first glance
err := db.QueryRow("SELECT email FROM users WHERE id = $1", id).Scan(&email)
if err != nil {
// The mistake: treating "not found" the same as "database is down"
return "", err
}
return email, nil
}
If you call this with a non-existent ID, the function returns a non-nil error. Your calling handler sees an error, assumes the database crashed, and returns a 500 Internal Server Error to the user. In reality, a missing user should probably trigger a 404 Not Found response.
The Idiomatic FixSince Go 1.13, the standard way to handle this is by using errors.Is() to check for the sql.ErrNoRows sentinel. This allows you to separate business logic from infrastructure failures.
The Correct Pattern```
import ( "database/sql" "errors" "fmt" )
func GetUserEmail(id int) (string, error) { var email string err := db.QueryRow("SELECT email FROM users WHERE id = $1", id).Scan(&email)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// Differentiate between a missing record and a connection crash
return "", fmt.Errorf("user %d does not exist", id)
}
// Handle actual connection timeouts or syntax errors
return "", fmt.Errorf("database failure: %w", err)
}
return email, nil
}
## Under the HoodWhy does `QueryRow` behave this way? It is a convenience wrapper. It returns a `*Row` object immediately, even before the query finishes executing. Because `Scan()` is where the database result actually maps to your variables, it is also the only place where the package can report that the result set was empty.
Always distinguish between these two states:
- **sql.ErrNoRows:** The query ran perfectly, but returned zero results.- **Other Errors:** Connection timeouts, authentication failures, or malformed SQL syntax.## Alternative: Using db.QueryIf you prefer to avoid sentinel errors entirely, use `db.Query()`. Unlike its counterpart, `Query` returns an iterator and does not throw an error if no rows are found.
rows, err := db.Query("SELECT email FROM users WHERE id = $1", id) if err != nil { return "", err } defer rows.Close()
if rows.Next() { var email string if err := rows.Scan(&email); err != nil { return "", err } return email, nil }
// No rows were found, but no error was triggered return "", nil
While `Query` is flexible, `QueryRow` remains the standard for single-item lookups because it is concise and manages the row lifecycle automatically.
## Verification Steps- **Test Edge Cases:** Run a unit test with an ID like `-1` or `0`. Verify your code returns your custom error instead of a raw SQL error.- **Check Log Levels:** Ensure that `ErrNoRows` is logged as `INFO`, while actual connection failures are logged as `ERROR`.- **Verify Status Codes:** If building an API, confirm the endpoint now returns a `404` status instead of a generic `500` error.## Lessons Learned- `sql.ErrNoRows` is a sentinel value, not a sign of a system failure.- Use `errors.Is()` to keep your error handling clean and readable.- Always handle `Scan()` errors surgically to provide better feedback to your users.

