Debugging your composition
You don’t see what happens inside a compose chain — what each Task returned, which were skipped, what your Tags carried, or what every Spot computed.
When something goes wrong, you want to see inside — without scattering console.log across every Task. There are a few ways to do that.
scope.get
Section titled “scope.get”compose(...).run() returns Promise<Scope> — await it to get a Scope with one method, scope.get(spot), that reads any Spot computed during the run. A Spot is any readable source in the composition: task.result, task.status, task.error, tag.value, anything from shape(...). The call returns undefined if the source wasn’t computed (a skipped or failed Task).
import { compose, createTask, createWire, tag } from "@app-compose/core"
const userId = tag<number>("userId")
const fetchUser = createTask({ name: "fetch-user", run: { fn: () => ({ id: 1 }) },})
const loadCart = createTask({ name: "load-cart", run: { context: userId.value, fn: () => { throw new Error("cart service is down") }, },})
;(async () => { const scope = await compose() .step(fetchUser) .step(createWire({ from: fetchUser.result.id, to: userId })) .step(loadCart) .run()
console.log(`[fetchUser.status]: ${scope.get(fetchUser.status)}`) console.log(`[fetchUser.result]: ${JSON.stringify(scope.get(fetchUser.result))}`) console.log(`[userId]: ${scope.get(userId.value)}`) console.log(`[loadCart.status]: ${scope.get(loadCart.status)}`) console.log(`[loadCart.error]: ${scope.get(loadCart.error)}`)})()Good for
- One place — all your reads cluster after the run.
Trade-offs
- Manual — you pick what to read and log, by hand.
- End-only — you see the final state of each Spot, not snapshots from mid-run.
Debug Task
Section titled “Debug Task”Add a Task that reads any Tasks, Tags, or Spots via context and logs them. Place it between .step(...) calls to see state at that point in the chain.
The catch: every context read must be wrapped in optional — optional(task.result), optional(tag.value), and so on. Without it, the debug Task is skipped along with any source that fails or is skipped — and you get no log.
debug from @app-compose/coda handles this wrapping for you. Pass any Tasks, Tags, or Spots; it returns a Task you can .step(...). A lint rule no-coda-debug flags every call so it doesn’t reach a commit.
import { debug } from "@app-compose/coda"import { compose, createTask, createWire, tag } from "@app-compose/core"
const userId = tag<number>("userId")
const fetchUser = createTask({ name: "fetch-user", run: { fn: () => ({ id: 1 }) },})
const loadCart = createTask({ name: "load-cart", run: { fn: () => { throw new Error("cart service is down") }, },})
compose() .step(fetchUser) .step(createWire({ from: fetchUser.result.id, to: userId })) // after fetchUser + wire .step(debug(fetchUser, userId)) .step(loadCart) // after loadCart .step(debug(loadCart)) .run()
console.warn("This sandbox flattens grouped logs.")console.warn("In your DevTools console, debug() output is nested and collapsible.")