Skip to Content
Course content

140: context.Background and context.TODO

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you've probably noticed that almost every meaningful function in a professional Go codebase takes ctx context.Context as its first argument. It's become the standard way we handle timeouts, cancellations, and request-scoped values. But this leads to a classic "chicken and egg" problem: if every function needs a context passed into it, who actually creates the first one?

The trap of the internal Background

When I'm reviewing code from developers new to Go, I often see a pattern where they realize a library function requires a context, but they don't want to "clutter" their own function signature by adding it. To solve this, they do something like this:

func GetUserPermissions(userID string) ([]string, error) {
    // "I just need a context to make this call work"
    ctx := context.Background() 
    return db.QueryPermissions(ctx, userID)
}

On the surface, this works. The code compiles, the query runs, and the tests pass. But you've just created a "detached" context. By calling context.Background() inside the function, you've severed the link between the caller and the operation. If the user closes their browser tab or the HTTP request times out, the GetUserPermissions function has no way of knowing. It will keep churning away at the database, wasting resources on a result that nobody is listening for anymore.

The better way is to embrace the "clutter." If QueryPermissions needs a context, GetUserPermissions should probably take one too. You pass the context down the chain from the very top—usually from the incoming HTTP request or the main entry point of your CLI tool. This ensures that a cancellation signal at the top ripples all the way down to the database driver.

Being honest with context.TODO

Now, you might be wondering why context.TODO() even exists if context.Background() does the same thing technically. If you look at the source code, they are virtually identical. The difference isn't technical; it's communicative.

I use context.Background() when I am certain this is the root of the tree. This is for your main() function, your top-level background workers, or the start of a test case. It says, "This is the intended starting point."

I use context.TODO() as a marker. Imagine you're refactoring a legacy project. You're adding context support to a deep call stack of twenty functions. You can't update all twenty signatures in one commit without breaking everything, but you need to update the leaf function (the one actually doing the I/O) right now.

func LegacyWrapper() {
    // I know this should be passed in, but I haven't 
    // updated the callers yet.
    result, err := FetchData(context.TODO()) 
    // ...
}

By using TODO(), you're leaving a breadcrumb for yourself and your teammates. When someone searches the codebase for ".TODO()", they'll find every spot where the context chain is broken. If you used Background() there, it would look intentional, and you'd likely never go back to fix the leak. It's essentially a compiler-approved "FIXME" comment.




📋 Practical Task

Refactoring the Leaky Permission Checker

You have a small piece of a system where a PermissionService is leaking database connections because it creates its own background contexts instead of respecting the request lifecycle. Your task is to refactor the code to properly propagate the context from the handler down to the data layer.

Requirements:

  • Modify GetPermissions to accept a context.Context argument.
  • Remove the local call to context.Background() inside GetPermissions and use the passed-in context instead.
  • Update the Handler function to pass the request context (via r.Context()) into the service.
  • In the main function, use context.Background() to initialize the starting context for the simulation.
package main

import (
	"context"
	"fmt"
	"net/http"
)

type PermissionService struct{}

// TODO: Refactor this to stop using context.Background() internally
func (s *PermissionService) GetPermissions(userID string) ([]string, error) {
	ctx := context.Background() 
	fmt.Printf("Fetching permissions for %s using context %v\n", userID, ctx)
	// Simulate a DB call that respects ctx
	return []string{"read", "write"}, nil
}

func Handler(w http.ResponseWriter, r *http.Request) {
	svc := &PermissionService{}
	// TODO: Pass the request context here
	perms, err := svc.GetPermissions("user-123")
	if err != nil {
		http.Error(w, "error", 500)
		return
	}
	fmt.Fprintf(w, "Permissions: %v", perms)
}

func main() {
	// The entry point: This is where context.Background() actually belongs
	fmt.Println("Server starting...")
	// Simulation of a request call
	// ...
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.