{adamcoding}
Part I
06
Chapter 6

When It Goes Wrong

The problem

movingAverage(prices, 200) on 50 prices. What should happen?

It cannot return an average - there isn't one. It has three options: crash, return a nonsense number, or say "I couldn't do that." Only one of those is acceptable in code that trades.

Errors are just values

Most languages use exceptions: something goes wrong, execution jumps somewhere else, and if nobody catches it the program dies. Go doesn't. In Go, an error is an ordinary value that a function returns alongside its answer.

go
func movingAverage(prices []float64, window int) ([]float64, error) {
	if window < 1 {
		return nil, fmt.Errorf("window must be at least 1, got %d", window)
	}
	if window > len(prices) {
		return nil, fmt.Errorf("window %d exceeds %d prices", window, len(prices))
	}

	result := make([]float64, 0, len(prices)-window+1)
	sum := 0.0
	for i := 0; i < window; i++ {
		sum += prices[i]
	}
	result = append(result, sum/float64(window))

	for i := window; i < len(prices); i++ {
		sum += prices[i]
		sum -= prices[i-window]
		result = append(result, sum/float64(window))
	}
	return result, nil
}

Two returns: the answer, and whether it worked. When things go well the error is nil - Go's word for "nothing here." When they don't, the answer is nil and the error explains why.

Calling it:

go
ma, err := movingAverage(prices, 200)
if err != nil {
	fmt.Println("Could not compute:", err)
	return
}
fmt.Println(ma)

This shape - call, check err != nil, handle it - is the Go idiom. You will write it thousands of times. People coming from other languages complain it's repetitive.

They're right that it's repetitive. They're wrong that it's bad, and here's why.

Why this is better than exceptions

Every failure is visible in the signature. func movingAverage(...) ([]float64, error) tells you it can fail. In a language with exceptions, any function might throw, and finding out which requires reading the implementation and everything it calls.

You cannot ignore it by accident. Go won't let you declare a variable and not use it. Skipping the error requires explicitly writing _, which is a visible decision rather than an oversight.

Handling is local. The error is dealt with next to the call that produced it, where you still know what you were trying to do. With exceptions, the failure surfaces somewhere up the stack, often in a place that has no idea what the original request was.

For code that manages money, the repetition buys something worth having: there is no such thing as a surprise failure path. Every way a function can fail is written into its type, and every caller has visibly decided what to do about it.

Guard clauses

Notice how movingAverage handles its problems first and returns immediately. That's a guard clause, and it's the shape you want:

go
func something(x float64) (float64, error) {
	if x < 0 {
		return 0, fmt.Errorf("x must be non-negative, got %f", x)
	}
	if x > 1000 {
		return 0, fmt.Errorf("x too large: %f", x)
	}
	// the real work, with all the bad cases already gone
	return x * 2, nil
}

The alternative is nesting the happy path deeper and deeper inside if blocks, which becomes unreadable after two levels. Deal with the bad cases and get them out of the way.

Writing useful error messages

Compare:

go
return nil, fmt.Errorf("invalid input")                          // useless
return nil, fmt.Errorf("window %d exceeds %d prices", window, len(prices))   // useful

The second tells you what the values were. At 3 a.m., reading a log, that difference is everything. Include the actual numbers. fmt.Errorf works exactly like Printf - same placeholders.

Wrapping errors

When you pass an error up, add context with %w:

go
func loadAndAverage(filename string, window int) ([]float64, error) {
	prices, err := loadPrices(filename)
	if err != nil {
		return nil, fmt.Errorf("loading %s: %w", filename, err)
	}

	ma, err := movingAverage(prices, window)
	if err != nil {
		return nil, fmt.Errorf("computing %d-bar average: %w", window, err)
	}
	return ma, nil
}

You get a chain describing what was being attempted at each level:

computing 200-bar average: window 200 exceeds 50 prices

%w specifically (rather than %v) keeps the original error inspectable, so code above can test what kind of failure it was.

Sentinel errors

For failures a caller might want to respond to specifically, define the error once:

go
import "errors"

var ErrInsufficientData = errors.New("insufficient data")

func movingAverage(prices []float64, window int) ([]float64, error) {
	if window > len(prices) {
		return nil, fmt.Errorf("%w: need %d, have %d",
			ErrInsufficientData, window, len(prices))
	}
	// ...
}

Now a caller can distinguish it:

go
ma, err := movingAverage(prices, 200)
if errors.Is(err, ErrInsufficientData) {
	// not fatal - just wait for more bars
	return
}
if err != nil {
	// something else went wrong
	log.Fatal(err)
}

errors.Is looks through the whole %w chain, so wrapping doesn't hide it.

panic, and when to use it

Go does have a crash-the-program mechanism:

go
panic("something has gone very wrong")

Some things panic on their own - dividing an integer by zero, or reading past the end of a slice.

Use `panic` almost never. It's for situations where continuing is worse than stopping: a programmer error that should have been impossible, or a startup failure that means the program can't function. Not for "the data was bad" or "the network hiccuped" - those are ordinary errors, and ordinary errors are values.

In a trading system specifically: a panic while holding a position leaves the position open with nothing watching it. Prefer an error you handle, a position you flatten, and a log line you can read afterwards.

Putting it together

go
package main

import (
	"errors"
	"fmt"
)

var ErrInsufficientData = errors.New("insufficient data")

func movingAverage(prices []float64, window int) ([]float64, error) {
	if window < 1 {
		return nil, fmt.Errorf("window must be at least 1, got %d", window)
	}
	if window > len(prices) {
		return nil, fmt.Errorf("%w: window %d, have %d prices",
			ErrInsufficientData, window, len(prices))
	}

	result := make([]float64, 0, len(prices)-window+1)
	sum := 0.0
	for i := 0; i < window; i++ {
		sum += prices[i]
	}
	result = append(result, sum/float64(window))

	for i := window; i < len(prices); i++ {
		sum += prices[i] - prices[i-window]
		result = append(result, sum/float64(window))
	}
	return result, nil
}

func main() {
	prices := []float64{100, 102, 101, 105, 103, 107, 106, 109}

	for _, window := range []int{3, 5, 20, 0} {
		ma, err := movingAverage(prices, window)
		switch {
		case errors.Is(err, ErrInsufficientData):
			fmt.Printf("window %d: not enough data yet\n", window)
		case err != nil:
			fmt.Printf("window %d: bad request: %v\n", window, err)
		default:
			fmt.Printf("window %d: %.2f\n", window, ma[len(ma)-1])
		}
	}
}

Three different outcomes, three different responses, none of them a crash. That's the chapter.

Exercises

6.1 Rewrite positionSize from 5.3 to return (float64, error), with a real error when the stop distance is zero. Handle it at the call site.

6.2 Write parsePrice(s string) (float64, error) using strconv.ParseFloat, rejecting negative and zero prices with your own message. Test it on "67410.50", "abc", "-5" and "".

6.3 Define ErrNoTrades and write expectancy(rMultiples []float64) (float64, error) returning it for an empty input. Show a caller distinguishing "no trades yet" from a real failure.

6.4 Chain three functions where each wraps the previous error with %w, and print the final message. Confirm you can still identify the original with errors.Is.

6.5 Harder. Write validateBar(open, high, low, close float64, volume int64) error returning a single error listing every problem found, not just the first. Check: high is at least open and close, low is at most open and close, no non-positive prices, no negative volume. (Hint: collect messages in a slice and join them.)


Solutions

6.1

go
func positionSize(equity, riskPct, entry, stop float64) (float64, error) {
	riskPerUnit := math.Abs(entry - stop)
	if riskPerUnit == 0 {
		return 0, fmt.Errorf("entry and stop are both %.2f: no risk distance", entry)
	}
	return (equity * riskPct / 100) / riskPerUnit, nil
}

6.2

go
func parsePrice(s string) (float64, error) {
	v, err := strconv.ParseFloat(s, 64)
	if err != nil {
		return 0, fmt.Errorf("parsing price %q: %w", s, err)
	}
	if v <= 0 {
		return 0, fmt.Errorf("price must be positive, got %f", v)
	}
	return v, nil
}

%q wraps the string in quotes, which makes an empty input visible in the log rather than looking like a missing word.

6.3

go
var ErrNoTrades = errors.New("no trades")

func expectancy(rMultiples []float64) (float64, error) {
	if len(rMultiples) == 0 {
		return 0, ErrNoTrades
	}
	return mean(rMultiples), nil
}

6.4 Each layer adds context; errors.Is(err, ErrInsufficientData) still returns true at the top, because %w preserves the chain.

6.5

go
func validateBar(open, high, low, close float64, volume int64) error {
	var problems []string

	if high < open || high < close {
		problems = append(problems, fmt.Sprintf(
			"high %.2f below open %.2f or close %.2f", high, open, close))
	}
	if low > open || low > close {
		problems = append(problems, fmt.Sprintf(
			"low %.2f above open %.2f or close %.2f", low, open, close))
	}
	if open <= 0 || high <= 0 || low <= 0 || close <= 0 {
		problems = append(problems, "non-positive price present")
	}
	if volume < 0 {
		problems = append(problems, fmt.Sprintf("negative volume %d", volume))
	}

	if len(problems) > 0 {
		return fmt.Errorf("invalid bar: %s", strings.Join(problems, "; "))
	}
	return nil
}

Reporting every problem at once matters for data validation - fixing one issue only to be told about the next is a miserable way to clean a dataset.