Today, writing errors/bank_withdrawal.go, I robbed my own bank.
What I wanted to understand
I knew Go returns errors instead of throwing them. What I had not understood is that creating an error does nothing on its own.
What I wrote
func withDrawMoney(balance int, amount int) (int, error) {
if amount <= 0 {
errors.New("invalid withdrawal amount") // created, never returned
}
remainingBalance := balance - amount
return remainingBalance, nil
}What happened
Transaction successful... -1000A withdrawal larger than the balance went through, and the account went negative.
Why
errors.New(...) builds an error value and hands it back. On its own line it
is simply discarded. Execution carried on to return remainingBalance, nil, so
main saw a nil error and reported success.
The fix
if amount <= 0 {
return 0, errors.New("invalid withdrawal amount")
}The return is the whole point. And because the function returns
(int, error), the int slot gets a zero value when the error is real.
Where this matters
Anywhere validation can fail — payments, inventory, auth. An error that is built but not returned is a silent success, which is worse than a crash: the caller never learns anything went wrong.