Fix

"context canceled"

Fix context canceled errors by checking ctx.Err() and handling the cancellation signal gracefully in your Go code.

The request timed out, but your code kept running

You write a background worker that fetches data from an external API. You attach a timeout so the request does not hang forever. The timeout fires. Your logs suddenly fill with context canceled errors. The worker panics, retries, or worse, keeps holding onto a database connection while the parent goroutine moves on. The error is not a bug in your logic. It is the expected signal that the work is no longer needed.

What context actually does

Go's context.Context is a lightweight container for request-scoped values, deadlines, and cancellation signals. Think of it like a walkie-talkie channel shared across a team. The person who starts the operation holds the master radio. When they press the cancel button, every radio on that channel crackles with the same message. Your code listens to that channel. When the message arrives, you stop what you are doing and clean up.

The context package does not manage goroutines. It does not kill them. It only broadcasts a signal. Your job is to check the signal and exit cleanly. This design keeps concurrency predictable. You never have to guess why a goroutine stopped. You always see the explicit cancellation path in the code.

Contexts form a tree. The root is context.Background(). Every time you call WithTimeout, WithCancel, or WithValue, you create a child node. Cancellation flows downward. If a parent cancels, all children cancel immediately. Children cannot cancel their parents. This one-way flow prevents accidental shutdowns and keeps the call stack traceable.

Contexts are cheap to create and pass. They are immutable. You never modify a context in place. You always derive a new one. This immutability means you can safely pass the same context to multiple goroutines without race conditions. The runtime handles the synchronization behind the scenes.

Context is plumbing. Run it through every long-lived call site.

A minimal example

package main

import (
	"context"
	"fmt"
	"time"
)

// DoWork simulates a long-running task that respects cancellation.
func DoWork(ctx context.Context) error {
	// Check if the context is already canceled before starting.
	if err := ctx.Err(); err != nil {
		return err
	}

	// Simulate work in small steps instead of one blocking call.
	for i := 0; i < 10; i++ {
		// Check cancellation between steps to avoid wasting CPU.
		if err := ctx.Err(); err != nil {
			return fmt.Errorf("work interrupted: %w", err)
		}

		// Do a small chunk of work.
		time.Sleep(100 * time.Millisecond)
	}

	return nil
}

func main() {
	// Create a context that cancels after 250 milliseconds.
	ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
	// Always call cancel to release resources, even if the timeout does not fire.
	defer cancel()

	// Run the work in a goroutine so we can observe the result.
	err := DoWork(ctx)
	if err != nil {
		fmt.Println("Result:", err)
	}
}

Walking through the cancellation signal

The program starts by creating a timeout context. context.WithTimeout returns a new context and a cancel function. The cancel function is your responsibility to call. When you defer it, Go guarantees the underlying timer stops and the memory frees, even if the timeout never triggers.

DoWork checks ctx.Err() immediately. If the deadline already passed or the parent canceled the request, the function returns early. Inside the loop, the function checks the error again after each simulated step. This is the core pattern. Break long operations into smaller pieces and poll the context between them.

When the 250 millisecond deadline passes, the context marks itself as done. The next call to ctx.Err() returns context deadline exceeded. The loop exits, the error wraps the original signal, and the function returns. The main goroutine prints the result and exits. No goroutine leaks. No hanging connections. Just a clean stop.

The runtime does not interrupt your goroutine. Go is not preemptive at the instruction level. Your code must voluntarily check the context. If you block on a channel read, a network call, or a file operation, you need a library that accepts a context, or you need to race the operation against ctx.Done() using a select statement.

Check the signal early. Exit fast. Do not fight the cancellation.

Realistic scenario: HTTP handler with database call

Real code rarely sleeps in a loop. It usually talks to databases, HTTP servers, or message queues. Those libraries already know how to read contexts. You just need to pass the context through and handle the return values correctly.

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"time"
)

// FetchUser retrieves a user from the database with a deadline.
func FetchUser(ctx context.Context, db *sql.DB, id int) (*User, error) {
	// Pass the context to the query so the driver can cancel the statement.
	row := db.QueryRowContext(ctx, "SELECT name, email FROM users WHERE id = $1", id)

	var u User
	// Scan maps the result to the struct. If the context cancels during the query,
	// the driver returns an error here instead of blocking forever.
	if err := row.Scan(&u.Name, &u.Email); err != nil {
		return nil, fmt.Errorf("query failed: %w", err)
	}

	return &u, nil
}

// User holds basic profile data.
type User struct {
	Name  string
	Email string
}

// HandleUserRequest is an HTTP handler that respects client timeouts.
func HandleUserRequest(w http.ResponseWriter, r *http.Request) {
	// The request already carries a context that cancels when the client disconnects.
	ctx := r.Context()

	// Add a server-side deadline to prevent slow queries from holding resources.
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	// Simulate a database connection.
	db, err := sql.Open("postgres", "postgres://localhost/mydb")
	if err != nil {
		http.Error(w, "db connection failed", http.StatusInternalServerError)
		return
	}
	defer db.Close()

	user, err := FetchUser(ctx, db, 42)
	if err != nil {
		// Check if the error came from context cancellation.
		if ctx.Err() != nil {
			log.Printf("request canceled: %v", ctx.Err())
			http.Error(w, "request timed out", http.StatusGatewayTimeout)
			return
		}
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "Found user: %s", user.Name)
}

The HTTP handler uses r.Context() as the parent. This context automatically cancels when the client closes the browser tab or the proxy drops the connection. Wrapping it with context.WithTimeout adds a hard server limit. The database driver listens to that context. If the query takes too long, the driver aborts the statement and returns an error. Your code checks ctx.Err() to distinguish between a cancellation and a genuine database failure.

Convention aside: context.Context always goes as the first parameter. Name it ctx. Functions that accept a context should respect cancellation and deadlines. Never store a context inside a struct. Contexts are meant to flow through call stacks, not live in object state.

Pass the context down. Do not stash it away.

Common pitfalls and how to avoid them

Contexts are simple, but a few patterns trip up new Go developers.

Passing a context to a blocking call that ignores it is the most common mistake. If you call time.Sleep or a third-party function that does not accept a context, the cancellation signal sits unread until the function returns. The compiler cannot catch this. You get a runtime hang instead of a clean exit. Break blocking calls into smaller steps, or use a channel with select to race against the context.

// WaitOrCancel blocks until the channel sends a value or the context cancels.
func WaitOrCancel(ctx context.Context, ch <-chan string) (string, error) {
	select {
	case msg := <-ch:
		return msg, nil
	case <-ctx.Done():
		// ctx.Done() closes when the context is canceled or times out.
		return "", ctx.Err()
	}
}

Another pitfall is treating context canceled as a fatal error. It is not. It is a control flow signal. If your code logs it as a critical failure, your monitoring dashboard fills with noise. Check ctx.Err() explicitly. Return it early. Let the caller decide how to handle it. The compiler will remind you if you ignore return values, but it will not stop you from misinterpreting the error type. You get context canceled or context deadline exceeded as plain strings. Compare them with errors.Is(err, context.Canceled) or errors.Is(err, context.DeadlineExceeded) for reliable checks.

Forgetting to call the cancel function leaks timers and memory. The context package uses a tree of nodes. Each WithTimeout or WithCancel adds a node. If you never call the returned cancel function, the parent node keeps the child alive. The garbage collector cannot free it because the timer is still running. Always defer the cancel function immediately after creation.

Type mismatches also cause friction. If you pass a *context.Context instead of context.Context, the compiler rejects the program with cannot use ctx (type *context.Context) as context.Context value in argument. Contexts are already reference types under the hood. Passing a pointer adds an unnecessary layer of indirection and breaks the standard library expectations. Pass the value directly.

Treat cancellation as expected flow. Log it at debug level, not error level.

When to use context vs alternatives

Use context.WithTimeout when you need a hard deadline for an operation like a database query or external API call. Use context.WithCancel when you want to manually stop a background task from another goroutine. Use context.WithValue only for request-scoped metadata like trace IDs or authentication tokens, and never for passing optional parameters. Use a plain select with ctx.Done() when you need to wait on multiple channels while respecting cancellation. Use sequential code without context when the operation is fast, local, and cannot be interrupted.

Context is not a mutex. Context is not a channel. Context is a signal. Respect the signal and your concurrency stays predictable.

Where to go next