Fix runtime error: index out of range [0] with length 0 in Go/Echo
This error occurs when Go code accesses an index on an empty slice, such as items[0] when the slice has length 0. In Echo handlers, this commonly happens when a database query returns no results and the code accesses the first element without checking. Fix it by checking the slice length before accessing elements by index.
Reading the Stack Trace
Here's what each line means:
- main.GetLatestOrder(0xc000182000): The GetLatestOrder handler at line 22 of order.go accessed index 0 on an empty slice.
- runtime error: index out of range [0] with length 0: The Go runtime detected an out-of-bounds access: trying to access the first element of a slice that has zero elements.
- http: panic serving 127.0.0.1:52412: The panic caused by the out-of-range access was caught by the HTTP server, which logged it and returned a 500 error.
Common Causes
1. Accessing first element of empty query result
A database query returns an empty slice, and the handler accesses [0] without checking the length first.
func GetLatestOrder(c echo.Context) error {
userID := c.Param("user_id")
orders, err := db.GetOrdersByUserID(userID)
if err != nil {
return c.JSON(500, map[string]string{"error": "database error"})
}
latest := orders[0] // panics if orders is empty
return c.JSON(200, latest)
}
2. Empty slice from split or filter
A string split or filter operation returns an empty slice and the code assumes at least one element exists.
func ParseHeader(c echo.Context) error {
auth := c.Request().Header.Get("Authorization")
parts := strings.Split(auth, " ")
token := parts[1] // panics if auth is empty or has no space
return c.JSON(200, map[string]string{"token": token})
}
The Fix
Check len(orders) before accessing orders[0]. If the slice is empty, return a 404 response instead of panicking. This simple bounds check prevents the runtime panic entirely.
func GetLatestOrder(c echo.Context) error {
userID := c.Param("user_id")
orders, err := db.GetOrdersByUserID(userID)
if err != nil {
return c.JSON(500, map[string]string{"error": "database error"})
}
latest := orders[0]
return c.JSON(200, latest)
}
func GetLatestOrder(c echo.Context) error {
userID := c.Param("user_id")
orders, err := db.GetOrdersByUserID(userID)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "database error"})
}
if len(orders) == 0 {
return c.JSON(http.StatusNotFound, map[string]string{"error": "no orders found"})
}
latest := orders[0]
return c.JSON(http.StatusOK, latest)
}
Testing the Fix
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
)
func TestGetLatestOrder_WithOrders(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/api/users/:user_id/orders/latest")
c.SetParamNames("user_id")
c.SetParamValues("1")
err := GetLatestOrder(c)
if err != nil {
t.Fatalf("handler returned error: %v", err)
}
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
}
func TestGetLatestOrder_NoOrders(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/api/users/:user_id/orders/latest")
c.SetParamNames("user_id")
c.SetParamValues("99999")
err := GetLatestOrder(c)
if err != nil {
t.Fatalf("handler returned error: %v", err)
}
if rec.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", rec.Code)
}
var body map[string]string
json.Unmarshal(rec.Body.Bytes(), &body)
if body["error"] != "no orders found" {
t.Errorf("expected 'no orders found', got '%s'", body["error"])
}
}
func TestGetLatestOrder_DoesNotPanic(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("handler panicked: %v", r)
}
}()
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/api/users/:user_id/orders/latest")
c.SetParamNames("user_id")
c.SetParamValues("nonexistent")
GetLatestOrder(c)
}
Run your tests:
go test ./...
Pushing Through CI/CD
git checkout -b fix/index-out-of-range,git add handlers/order.go handlers/order_test.go,git commit -m "fix: check slice length before accessing first element",git push origin fix/index-out-of-range
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: go mod download
- run: go vet ./...
- run: go test -race -coverprofile=coverage.out ./...
- run: go build ./...
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
go get github.com/bugstack/sdk
Step 2: Initialize
import "github.com/bugstack/sdk"
func init() {
bugstack.Init(os.Getenv("BUGSTACK_API_KEY"))
}
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Run go test ./... locally to confirm the bounds check works.
- Run go vet ./... to catch other potential issues.
- Open a pull request with the length check fix.
- Have a teammate review and approve the PR.
- Merge to main and verify the endpoint returns proper 404 responses in staging.
Frequently Asked Questions
BugStack runs go test with race detection and bounds-checking analysis, verifies all slice accesses are guarded by length checks, and confirms no panics occur.
All fixes are submitted as pull requests with CI validation. Go's compiler and test suite catch issues before your team reviews and merges.
Slice lengths are determined at runtime, so the compiler cannot statically verify index bounds. Go adds runtime bounds checking that panics on invalid access.
No. Using recover to catch bounds panics masks bugs. Always check len() before indexing. recover should only be used as a last-resort safety net in middleware.