How It Works Features Pricing Blog Error Guides
Log In Start Free Trial
Go · Go

Fix ContextCanceled: context canceled in Go

This error occurs when an operation's context is canceled, typically because the HTTP client disconnected, a parent context timed out, or cancel() was called explicitly. Fix it by checking for context cancellation errors and handling them gracefully instead of treating them as unexpected failures. Log at debug level for client disconnects.

Reading the Stack Trace

goroutine 34 [running]: runtime/debug.Stack() /usr/local/go/src/runtime/debug/stack.go:24 +0x5e main.fetchData(0x14000226000) /app/services/data.go:28 +0x2d8 context.(*cancelCtx).Err(0x14000196040) /usr/local/go/src/context/context.go:500 +0x24 database/sql.(*DB).QueryContext(0x14000118300, {0x1029f0ea0, 0x14000196040}, {0x1028f1e60, 0x20}, {0x0, 0x0, 0x0}) /usr/local/go/src/database/sql/sql.go:1716 +0x1e4 net/http.(*conn).serve(0x14000196040, {0x1029f0ea0, 0x14000196060}) /usr/local/go/src/net/http/server.go:2009 +0x5f0

Here's what each line means:

Common Causes

1. Client disconnected during request

The HTTP client closed the connection (browser tab closed, timeout), canceling the request context.

func fetchData(c *gin.Context) {
	rows, err := db.QueryContext(c.Request.Context(), longQuery)
	if err != nil {
		log.Printf("ERROR: %v", err) // logs client disconnects as errors
		c.JSON(500, gin.H{"error": "database error"})
		return
	}
}

2. Parent context canceled too early

A parent context is canceled before child operations complete, cascading cancellation to all derived contexts.

func handler(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
	cancel() // called immediately — all operations using ctx are canceled
	result, err := slowOperation(ctx)
}

3. cancel() called in defer before result used

Using defer cancel() correctly, but the error is not distinguished from other failures.

func getData(ctx context.Context) ([]byte, error) {
	resp, err := http.Get(url) // does not use ctx
	if err != nil {
		return nil, err // could be context.Canceled — not checked
	}
	return io.ReadAll(resp.Body)
}

The Fix

Check for context.Canceled and context.DeadlineExceeded separately. Client disconnects (Canceled) should be logged at debug level and not trigger error alerts. Deadline exceeded should return 504. Only log unexpected database errors as errors.

Before (broken)
func fetchData(c *gin.Context) {
	rows, err := db.QueryContext(c.Request.Context(), longQuery)
	if err != nil {
		log.Printf("ERROR: %v", err)
		c.JSON(500, gin.H{"error": "database error"})
		return
	}
	defer rows.Close()
	// process rows...
}
After (fixed)
func fetchData(c *gin.Context) {
	rows, err := db.QueryContext(c.Request.Context(), longQuery)
	if err != nil {
		if errors.Is(err, context.Canceled) {
			log.Printf("DEBUG: client disconnected during query")
			return // client is gone, no response needed
		}
		if errors.Is(err, context.DeadlineExceeded) {
			c.JSON(http.StatusGatewayTimeout, gin.H{"error": "query timed out"})
			return
		}
		log.Printf("ERROR: database query failed: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
		return
	}
	defer rows.Close()
	// process rows...
}

Testing the Fix

package services_test

import (
	"context"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/assert"
)

func TestFetchData_ContextCanceled(t *testing.T) {
	db, mock, _ := sqlmock.New()
	defer db.Close()

	mock.ExpectQuery("SELECT").WillReturnError(context.Canceled)

	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	_, err := fetchDataWithContext(ctx, db)
	assert.ErrorIs(t, err, context.Canceled)
}

func TestFetchData_DeadlineExceeded(t *testing.T) {
	db, mock, _ := sqlmock.New()
	defer db.Close()

	mock.ExpectQuery("SELECT").WillReturnError(context.DeadlineExceeded)

	_, err := fetchDataWithContext(context.Background(), db)
	assert.ErrorIs(t, err, context.DeadlineExceeded)
}

func TestFetchData_Success(t *testing.T) {
	db, mock, _ := sqlmock.New()
	defer db.Close()

	rows := sqlmock.NewRows([]string{"id"}).AddRow(1)
	mock.ExpectQuery("SELECT").WillReturnRows(rows)

	result, err := fetchDataWithContext(context.Background(), db)
	assert.NoError(t, err)
	assert.NotNil(t, result)
}

Run your tests:

go test ./services/... -v

Pushing Through CI/CD

git checkout -b fix/go-context-canceled,git add services/data.go services/data_test.go,git commit -m "fix: distinguish context cancellation from database errors",git push origin fix/go-context-canceled

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 context handling works.
  2. Open a pull request with the context error handling changes.
  3. Wait for CI checks to pass on the PR.
  4. Have a teammate review and approve the PR.
  5. Merge to main and verify in staging.

Frequently Asked Questions

BugStack tests with simulated context cancellation and deadline scenarios, validates error classification logic, and ensures no false error alerts are triggered before marking it safe to deploy.

BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.

These are usually normal — clients close browsers, mobile apps background, and load balancers drop idle connections. Log them at debug level, not error.

Canceled means cancel() was called explicitly (often client disconnect). DeadlineExceeded means the context's timeout or deadline was reached.