Skip to content

Context — Cancellation and Deadlines

In the browser/Node.js you use AbortController to cancel a fetch or any operation that accepts an AbortSignal:

const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // cancel after 5 s
const res = await fetch(url, { signal: controller.signal });

In Go, context.Context plays the same role — and goes further. It is a standard interface that carries a cancellation signal, an optional deadline, and arbitrary request-scoped values. You pass it as the first argument to every I/O function in the call chain so that a cancellation at the top automatically propagates all the way down.

context.WithCancel returns a new context and a cancel function. Calling cancel closes the ctx.Done() channel, which every goroutine listening on that context will see:

TypeScript
// TypeScript: AbortController
const controller = new AbortController();
const { signal } = controller;
async function worker(signal: AbortSignal) {
// signal.aborted becomes true when abort() is called
while (!signal.aborted) {
await doWork();
}
console.log("Worker cancelled");
}
controller.abort(); // broadcast cancellation
Go
// Go: context.WithCancel
package main
import (
"context"
"fmt"
"sync"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
select {
case <-ctx.Done(): // unblocks when cancel() is called
fmt.Printf("Worker %d cancelled: %v\n", id, ctx.Err())
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
cancel() // broadcast cancellation to all workers
wg.Wait()
fmt.Println("All workers stopped")
}

For operations that must complete within a deadline, use context.WithTimeout. Go automatically cancels the context when the timer expires:

TypeScript
// TypeScript: timeout via Promise.race
async function withTimeout<T>(
fn: () => Promise<T>,
ms: number
): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
);
return Promise.race([fn(), timeout]);
}
Go
// Go: context.WithTimeout
package main
import (
"context"
"fmt"
"sync"
"time"
)
func slowOperation(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
select {
case <-time.After(5 * time.Second): // simulated slow work
fmt.Println("Operation complete")
case <-ctx.Done():
fmt.Println("Operation timed out:", ctx.Err())
}
}
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
100*time.Millisecond, // deadline: 100 ms
)
defer cancel() // always defer cancel — prevents resource leak
var wg sync.WaitGroup
wg.Add(1)
go slowOperation(ctx, &wg)
wg.Wait()
}
TypeScript
// TypeScript: pass request-scoped data via function arguments
async function handler(req: Request) {
const userId = req.headers.get('x-user-id');
await processRequest(userId);
}
Go
// Go: context.WithValue carries data through the call chain
package main
import (
"context"
"fmt"
)
type contextKey string
const userIDKey contextKey = "userID"
func processRequest(ctx context.Context) {
userID := ctx.Value(userIDKey)
fmt.Println("Processing for user:", userID)
}
func main() {
ctx := context.WithValue(
context.Background(),
userIDKey,
"user-123",
)
processRequest(ctx)
}
package main
import (
"context"
"fmt"
"sync"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
cancel()
wg.Wait()
fmt.Println("All workers stopped")
}
What JavaScript API is most analogous to context.Context?
Why should cancel() always be deferred?
How should a context be passed through a Go call chain?
Which channel closes when a context is cancelled?