fp-go-context
IBM/fp-go/skills/fp-go-context/SKILL.md
Use this skill when working with Go's context.Context in fp-go code (github.com/IBM/fp-go/v2/context/...). Trigger on mentions of context.Context in fp-go pipelines, request-scoped values, ctx.Value, context.WithValue, context keys, AskValue, WithValue, timeouts or deadlines (WithTimeout, WithDeadline, context.WithTimeout, defer cancel), cancellation, Local / LocalIOK / LocalIOResultK, WithContext / WithContextK, request-scoped loggers, converting func(ctx, ...) (T, error) functions to ReaderIOResult, or reviewing code that threads ctx by hand.
What's in it
- fp-go Context Handling
- Core Principle
- Before You Generate
- Packages and Availability
- 1. Running at the Edge
- 2. Bridging func(ctx, …) (T, error)
- 3. Reading the Context
- Keys and Accessors
- 4. Scoping the Context
- Deriving the Context from an Effect
- 5. Cancellation
- 6. Request-Scoped Logger
- 7. Context in Effect
- 8. Outside Pipelines
- 9. What Belongs in the Context
- 10. Testing
- Common Mistakes
- Review Checklist
- Import Reference
---
name: fp-go-context
description: >-
Use this skill when working with Go's context.Context in fp-go code
(github.com/IBM/fp-go/v2/context/...). Trigger on mentions of
context.Context in fp-go pipelines, request-scoped values, ctx.Value,
context.WithValue, context keys, AskValue, WithValue, timeouts or deadlines
(WithTimeout, WithDeadline, context.WithTimeout, defer cancel),
cancellation, Local / LocalIOK / LocalIOResultK, WithContext / WithContextK,
request-scoped loggers, converting func(ctx, ...) (T, error) functions to
ReaderIOResult, or reviewing code that threads ctx by hand.
---
# fp-go Context Handling
## Core Principle
In imperative Go the context is threaded by hand: every function takes `ctx` first, values are read with `ctx.Value(k).(T)`, scopes are opened with `ctx, cancel := context.WithTimeout(...); defer cancel()`.
In fp-go the context is the **Reader environment**. A computation is a *description* `func(context.Context) …` that runs only when a context is supplied. Therefore:
1. **Pipelines never mention `ctx`.** Build them from operators; the context flows implicitly.
2. **Supply the context exactly once, at the edge** — HTTP handler (`r.Context()`), `main`, or a test (`t.Context()`).
3. **Read the context with operators** (`Ask`, `FromReader`, `AskValue`), never with `ctx.Value(k).(T)`.
4. **Scope the context with operators** (`WithValue`, `WithTimeout`, `WithDeadline`, `Local`), never with `context.With*` + `defer cancel()`.
5. **Only request-scoped data goes into the context.** Dependencies (DB, clients, config) belong in `Effect[Deps, A]`.
## Before You Generate
fp-go is low-frequency in training data, so signatures are easy to misremember.
For any combinator not shown below, look it up via the fp-go MCP server's
`search_examples` / `get_example` tools (see the **fp-go-mcp** skill) instead of
guessing. After writing code, run `go build ./...` and `go vet ./...` and fix any
type-parameter or argument-order errors before presenting it.
## Packages and Availability
| Package | Shape | Use when |
|---------|-------|----------|
| `context/readerioresult` (`RIO`) | `func(ctx) func() Result[A]` | default for services: IO + errors + context |
| `context/readerresult` (`RR`) | `func(ctx) Result[A]` | synchronous, can fail |
| `context/readerio` (`RIOC`) | `func(ctx) func() A` | IO that cannot fail |
| `context/statereaderioresult` (`SRIO`) | `func(S) func(ctx) func() Result[Pair[S, A]]` | explicit state + context |
| `idiomatic/context/readerresult` (`IRR`) | `func(ctx) (A, error)` | high-performance, native `(A, error)` |
| `context/reader` (`CR`) | `func(ctx) A` | plain building blocks for use *outside* pipelines |
| `effect` (`EF`) | `func(C) RIO.ReaderIOResult[A]` | typed deps `C` **plus** the runtime context |
| Operator | RIO | RR | RIOC | SRIO | IRR | CR |
|----------|:---:|:--:|:----:|:----:|:---:|:--:|
| `Ask()` | ✓ | ✓ | ✓ | | ✓ | |
| `FromReader(f)` / `Asks(f)` | ✓ | ✓ | ✓ | `Asks` | ✓ | |
| `AskValue[V](key)` | ✓ | ✓ | ✓ | ✓ (`[S, V]`) | ✓ | ✓ |
| `WithValue[A](key, v)` | ✓ | ✓ | ✓ | ✓ (`[S, A]`) | ✓ | Kleisli form |
| `WithTimeout[A](d)` / `WithDeadline[A](t)` | ✓ | ✓ | ✓ | ✓ (`[S, A]`) | ✓ | |
| `Local[A](f)` | ✓ | ✓ | ✓ | ✓ | ✓ | |
| `LocalIOK` / `LocalIOResultK` | ✓ / ✓ | | ✓ / | | | |
| `WithContext` / `WithContextK` | ✓ | ✓ | | | ✓ | |
| `NopCancel(ctx)` | | | | | | ✓ |
`Local`'s argument is `func(ctx) Pair[context.CancelFunc, context.Context]` in the standard packages and `func(ctx) (context.Context, context.CancelFunc)` in `IRR`.
## 1. Running at the Edge
```go
// HTTP handler: the request context is supplied once
func handler(w http.ResponseWriter, r *http.Request) {
res := handleRequest(r)(r.Context())() // Result[Response] — ONE value
resp, err := R.Unwrap(res) // bridge back to (A, error)
// ...
}
// Tests: use t.Context(), cancelled automatically when the test ends
res := pipeline(t.Context())()
```
Never capture a `ctx` in a closure and never store it in a struct; pass it only when running.
## 2. Bridging `func(ctx, …) (T, error)`
Existing context-first Go functions are lifted, not wrapped by hand:
```go
// func(context.Context, string) ([]byte, error) -> func(string) RIO.ReaderIOResult[[]byte]
fetch := RIO.Eitherize1(fetchBytes)
// idiomatic package: the shape is already native
fetchI := IRR.From1(fetchBytes) // func(string) IRR.ReaderResult[[]byte]
// and back, for APIs that expect the Go shape
fetchGo := RIO.Uneitherize1(fetch) // func(context.Context, string) ([]byte, error)
```
The lifted function receives the pipeline's (possibly scoped) context — timeouts and values applied with the operators below reach it automatically.
## 3. Reading the Context
| Need | Operator | Result |
|------|----------|--------|
| whole context | `RIO.Ask()` | `ReaderIOResult[context.Context]` |
| pure projection | `RIO.FromReader(f)` | `ReaderIOResult[A]` |
| one typed value | `RIO.AskValue[V](key)` | `ReaderIOResult[Option[V]]` |
`AskValue` **never panics and never fails**: `Some(v)` if the key holds a `V`, `None` if it is absent *or holds another type*. The caller decides what "missing" means.
### Keys and Accessors
Use an unexported key type — never plain strings or exported types — and keep one read and one write accessor next to each key, so the rest of the code never touches the key:
```go
type ctxKey int
const (
userKey ctxKey = iota
requestIDKey
)
var errNoUser = errors.New("no authenticated user in context")
// optional value: None -> default
func requestID() RIO.ReaderIOResult[string] {
return F.Pipe1(
RIO.AskValue[string](requestIDKey),
RIO.Map(O.GetOrElse(LZ.Of("-"))),
)
}
// required value: None -> error
func requireUser() RIO.ReaderIOResult[User] {
return F.Pipe1(
RIO.AskValue[User](userKey),
RIO.Chain(RIO.FromOption[User](F.Constant(errNoUser))),
)
}
// writer: scopes the value to the wrapped computation
func withUser[A any](u User) RIO.Operator[A, A] {
return RIO.WithValue[A](userKey, u)
}
```
`RR` and `IRR` have no `FromOption`, so the required-value accessor looks slightly different there:
```go
// RR: lift result.FromOption (Option[User] -> Result[User]) into the reader
func requireUserRR() RR.ReaderResult[User] {
return F.Pipe1(
RR.AskValue[User](userKey),
RR.ChainEitherK(R.FromOption[User](LZ.Of(errNoUser))),
)
}
// IRR: ChainOptionK expects an idiomatic (B, bool) Kleisli; O.Unwrap turns the Option into that tuple
func requireUserIRR() IRR.ReaderResult[User] {
return F.Pipe1(
IRR.AskValue[User](userKey),
IRR.ChainOptionK[O.Option[User], User](LZ.Of(errNoUser))(O.Unwrap[User]),
)
}
```
The `IRR` form needs the explicit `[O.Option[User], User]` because `ChainOptionK` receives only the `onNone` error first, so Go cannot infer `A` and `B` from it.
## 4. Scoping the Context
Scoping operators run the wrapped computation with a **derived** context. The caller's context is never modified, and the derived context's cancel function is **always** released when the computation completes — no `defer cancel()`, no leaks.
| Operator | Derives with | Notes |
|----------|--------------|-------|
| `WithValue[A](key, v)` | `context.WithValue` | inner values shadow outer ones for the same key |
| `WithTimeout[A](d)` | `context.WithTimeout` | relative to when the computation *runs*, not when the operator is built |
| `WithDeadline[A](t)` | `context.WithDeadline` | an earlier parent deadline still wins |
| `Local[A](f)` | anything | general form; the others are built on it |
They are ordinary operators and compose in `Pipe`. The context flows **from the last operator inwards**, so the last one is applied first:
```go
func handleRequest(r *http.Request) RIO.ReaderIOResult[Response] {
return F.Pipe4(
requireUser(),
RIO.Chain(loadProfile),
RIO.WithTimeout[Response](5*time.Second), // bounds the work above
withUser[Response](userFromRequest(r)), // visible to everything above
RIO.WithValue[Response](requestIDKey, r.Header.Get(HD.XRequestID)),
)
}
```
Scope as narrowly as the requirement: put `WithTimeout` directly after the step it should bound, not around the whole handler, if only one call needs it.
### Deriving the Context from an Effect
When the new context value itself needs IO (generating an ID, loading a token), use `LocalIOK` / `LocalIOResultK` with the `context/reader` building blocks:
```go
// uuid.NewString is a func() string, i.e. already an IO[string]
func addRequestID(ctx context.Context) IO.IO[RIO.ContextCancel] {
return F.Pipe1(
uuid.NewString,
IO.Map(F.Flow3(
CR.WithValue[string](requestIDKey), // string -> Endomorphism[context.Context]
RD.Read[context.Context](ctx), // apply it to ctx
CR.NopCancel, // -> Pair[CancelFunc, context.Context]
)),
)
}
scoped := RIO.LocalIOK[Response](addRequestID)(handler)
```
`LocalIOResultK` does the same with a fallible derivation; on failure the wrapped computation does not run.
## 5. Cancellation
- In `RIO`, `RR` and `IRR`, **`Chain` checks the context before each step**: once the context is cancelled, the next step does not run and the result is `Left(context.Cause(ctx))`. `Map` does not check (pure functions are cheap).
- `RIOC` (`context/readerio`) has no error channel, so it cannot short-circuit; use it only for IO that is fine to complete.
- `WithContext(ma)` / `WithContextK(f)` add the same check explicitly, e.g. before an expensive first step.
- `Delay`, retries, `Bracket` / `WithResource` and the HTTP client observe cancellation.
- **Leaf computations that block** (loops, `select`, custom IO) must watch `ctx.Done()` themselves and return `ctx.Err()` / `context.Cause(ctx)`:
```go
func waitForJob(id string) RIO.ReaderIOResult[Job] {
return func(ctx context.Context) RIO.IOResult[Job] {
return func() R.Result[Job] {
select {
case job := <-jobs(id):
return R.Of(job)
case <-ctx.Done():
return R.Left[Job](context.Cause(ctx))
}
}
}
}
```
## 6. Request-Scoped Logger
`logging.WithLogger(l)` already has `Local`'s argument shape, so a request logger is one operator. All context-aware logging (`TapSLog`, `SLog`, `LogEntryExit`) inside picks it up:
```go
F.Pipe1(
pipeline,
RIO.Local[Response](logging.WithLogger(slog.Default().With("requestID", id))),
)
```
Read it with `logging.GetLoggerFromContext` (falls back to the global logger). For a logger under your own key: `F.Flow2(CR.AskValue[*slog.Logger](myKey), O.GetOrElse(slog.Default))`.
## 7. Context in `Effect`
`C` holds the dependencies and `context.Context` holds the request scope; see the `fp-go-effect` skill for designing `C`.
`Effect[C, A]` is `func(C) RIO.ReaderIOResult[A]`: `C` carries typed dependencies, `context.Context` is still the runtime context supplied by `RunSync(…)(ctx)`.
- `EF.Local`, `EF.Ask`, `EF.Asks` operate on **`C`**, not on `context.Context`.
- `EF.Eitherize(func(C, context.Context) (T, error))` receives both.
- To scope the runtime context of an `Effect`, lift the `RIO` operator over `C` with `RD.Map` (plain `reader.Map`):
```go
bounded := RD.Map[Deps](RIO.WithTimeout[User](2*time.Second))(fetchUserEffect) // Effect[Deps, User]
tagged := RD.Map[Deps](RIO.WithValue[User](requestIDKey, id))(fetchUserEffect)
```
(`RD` = `github.com/IBM/fp-go/v2/reader`.)
## 8. Outside Pipelines
When a plain `context.Context` is genuinely needed (handing it to a non-fp-go API, building a fixture), use the `context/reader` building blocks instead of the standard library calls, so reads and writes stay symmetric:
```go
ctx2 := CR.WithValue[User](userKey)(u)(ctx) // context.WithValue, curried
user := CR.AskValue[User](userKey)(ctx2) // Option[User]
cc := CR.NopCancel(ctx2) // Pair[CancelFunc, ctx] for Local-shaped APIs
```
For optics composition over the context: `lenses.AtContext[V](key)` (`optics/lenses`) is a lens `context.Context → Option[V]`.
## 9. What Belongs in the Context
| Data | Where |
|------|-------|
| request / correlation / trace IDs | context (`WithValue`) |
| authenticated principal | context (`WithValue`) |
| request-scoped logger | context (`Local(logging.WithLogger(l))`) |
| deadlines, cancellation | context (`WithTimeout`, `WithDeadline`) |
| DB handles, HTTP clients, repositories | `Effect[Deps, A]` |
| configuration, feature flags | `Effect[Deps, A]` or function parameters |
| per-call inputs | function parameters (Kleisli arrows) |
A value that is required for the program to be correct is a dependency, not a context value — the compiler cannot check that it was provided.
## 10. Testing
```go
func TestRequireUser(t *testing.T) {
// present
res := F.Pipe1(requireUser(), withUser[User](alice))(t.Context())()
assert.Equal(t, R.Of(alice), res)
// missing -> error, no panic
assert.Equal(t, R.Left[User](errNoUser), requireUser()(t.Context())())
}
func TestTimeout(t *testing.T) {
res := F.Pipe1(waitForJob("slow"), RIO.WithTimeout[Job](10*time.Millisecond))(t.Context())()
assert.Equal(t, R.Left[Job](context.DeadlineExceeded), res)
}
```
- Always run with `t.Context()`, not `context.Background()`.
- Inject context values with the same `with…` accessors production code uses.
- Test the missing-value path of every required value.
- Keep timeouts in tests short (milliseconds) and assert on `context.DeadlineExceeded`.
## Common Mistakes
| ❌ Avoid | ✅ Prefer | Why |
|---------|----------|-----|
| `ctx.Value(k).(T)` | `RIO.AskValue[T](k)` + `GetOrElse` / `FromOption` | assertion panics when missing or mistyped |
| `v, _ := ctx.Value(k).(T)` then `if v == ""` | `AskValue` + `Option` | zero value is indistinguishable from "missing" |
| `context.WithValue(ctx, "user", u)` | unexported `ctxKey` + `WithValue[A](userKey, u)` | string keys collide across packages |
| `ctx, cancel := context.WithTimeout(ctx, d); defer cancel()` around `pipeline(ctx)()` | `F.Pipe1(pipeline, RIO.WithTimeout[A](d))` | scoping belongs to the description, not the call site |
| `Local` with `ctx2, _ := context.WithTimeout(…)` | `WithTimeout[A](d)` | discarded cancel leaks a timer until the deadline |
| `func(ctx) … { return pipeline(ctx) }` wrappers | return the pipeline value itself | the Reader already *is* that function |
| capturing `ctx` in a closure or struct field | supply it when running | the pipeline silently uses a stale context |
| DB / client stored in the context | `Effect[Deps, A]` | untyped, unchecked, hard to test |
| `pair.Unpack(logging.WithLogger(l)(ctx))` + `defer` | `RIO.Local[A](logging.WithLogger(l))` | already `Local`-shaped |
| `context.Background()` inside library code or tests | the caller's context / `t.Context()` | breaks cancellation and deadlines |
| blocking leaf that ignores `ctx.Done()` | `select` on `ctx.Done()` | timeouts and cancellation cannot take effect |
## Review Checklist
- [ ] No `ctx.Value(...)` type assertions; values are read with `AskValue`.
- [ ] Keys are unexported types; each key has a read and a write accessor.
- [ ] Required values are turned into errors explicitly (`FromOption` / `ChainOptionK`), optional ones defaulted.
- [ ] No `context.WithValue` / `WithTimeout` / `WithDeadline` / `WithCancel` inside or around pipelines — operators are used instead.
- [ ] No discarded cancel functions.
- [ ] The context is supplied once, at the edge; not captured or stored.
- [ ] Blocking leaf computations observe `ctx.Done()`.
- [ ] Dependencies live in `Effect[Deps, A]`, not in the context.
- [ ] Tests run with `t.Context()` and cover the missing-value and timeout paths.
## Import Reference
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
RR "github.com/IBM/fp-go/v2/context/readerresult"
RIOC "github.com/IBM/fp-go/v2/context/readerio"
SRIO "github.com/IBM/fp-go/v2/context/statereaderioresult"
IRR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
CR "github.com/IBM/fp-go/v2/context/reader"
EF "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/result"
IO "github.com/IBM/fp-go/v2/io"
LZ "github.com/IBM/fp-go/v2/lazy"
RD "github.com/IBM/fp-go/v2/reader"
HD "github.com/IBM/fp-go/v2/http/headers"
LS "github.com/IBM/fp-go/v2/optics/lenses"
"github.com/IBM/fp-go/v2/logging"
)
```
See also the `context` package documentation (`go doc github.com/IBM/fp-go/v2/context`) and the `fp-go`, `fp-go-logging` and `fp-go-http` skills.
More agent context in IBM/fp-go
11 other files this repository gives its agents.
AGENTS.md
llms.txt
Skill
- fp-go-effectskills/fp-go-effect/SKILL.md
- fp-go-httpskills/fp-go-http/SKILL.md
- fp-go-lensskills/fp-go-lens/SKILL.md
- fp-go-loggingskills/fp-go-logging/SKILL.md
- fp-go-mcpskills/fp-go-mcp/SKILL.md
- fp-go-pattern-matchingskills/fp-go-pattern-matching/SKILL.md
- fp-go-pipe-flowskills/fp-go-pipe-flow/SKILL.md
- fp-go-pr-reviewskills/fp-go-pr-review/SKILL.md
- fp-goskills/fp-go/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

