How It Works Features Pricing Blog
Log In Start Free Trial
Go/Echo · Go

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

2026/04/10 14:30:45 http: panic serving 127.0.0.1:52412: runtime error: index out of range [0] with length 0 goroutine 21 [running]: net/http.(*conn).serve.func1() /usr/local/go/src/net/http/server.go:1898 +0xb0 runtime.gopanic({0x6e4a20?, 0xc0001a8048?}) /usr/local/go/src/runtime/panic.go:770 +0x132 main.GetLatestOrder(0xc000182000) /app/handlers/order.go:22 +0x18f github.com/labstack/echo/v4.(*Echo).add.func1(0xc000182000) /app/vendor/github.com/labstack/echo/v4/echo.go:584 +0x72 github.com/labstack/echo/v4.(*Echo).ServeHTTP(0xc0000b6000, {0x7550a0?, 0xc000188460}, 0xc000186100) /app/vendor/github.com/labstack/echo/v4/echo.go:669 +0x3f5 net/http.serverHandler.ServeHTTP({0xc0000b4000?}, {0x7550a0, 0xc000188460}, 0xc000186100) /usr/local/go/src/net/http/server.go:3137 +0x8e

Here's what each line means:

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.

Before (broken)
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)
}
After (fixed)
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:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. 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:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. 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)

  1. Run go test ./... locally to confirm the bounds check works.
  2. Run go vet ./... to catch other potential issues.
  3. Open a pull request with the length check fix.
  4. Have a teammate review and approve the PR.
  5. 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.