microsoft/aspire · error · AppHostUsageError
Argument ' ' passed to capability ' ' contains a circular…
Error message
Argument '${path}' passed to capability '${capabilityId}' contains a circular reference. Circular references are not supported by the AppHost transport. What it means
The TypeScript AppHost transport validates every argument before sending it over JSON-RPC. validateValue walks each argument object graph with an 'ancestors' set; if an object appears on the path to itself (a cycle), it cannot be serialized, so the transport throws instead of hanging or producing infinite output. The AppHost transport only accepts acyclic, JSON-like payloads.
Solutions
- Remove the cycle: drop parent/back pointers or replace them with ids before passing the argument.
- Send a plain projection of the data (structuredClone won't work on cycles; build a shallow DTO with only needed fields).
- Pass JSON.parse(JSON.stringify()) of a cycle-free version, or encode cyclic links as string ids the capability resolves server-side.
- Wrap the call in try/catch to surface the offending path shown in the message and fix that specific field.
Example fix
// before
const node = { name: 'root' };
node.parent = node; // circular
await client.capability('deploy', { config: node });
// after
const node = { name: 'root', parentId: null }; // break cycle into an id reference
await client.capability('deploy', { config: node }); Defensive patterns
Strategy: validation
Validate before calling
function isAcyclic(value, seen = new WeakSet()) {
if (typeof value !== 'object' || value === null) return true;
if (seen.has(value)) return false;
seen.add(value);
return (Array.isArray(value) ? value : Object.values(value)).every(v => isAcyclic(v, seen));
}
// call before: if (!isAcyclic(args.config)) throw new Error('circular argument'); Type guard
const isPlainSerializable = (v: unknown, seen = new WeakSet<object>()): v is Record<string, unknown> => typeof v === 'object' && v !== null && !seen.has(v) && (seen.add(v), Object.values(v).every(c => isPlainSerializable(c, seen)));
Try / catch
try {
await client.capability('deploy', { config });
} catch (e) {
if (String((e as Error).message).includes('circular reference')) {
// sanitize or project the offending path from the message and retry with a DTO
}
} Prevention
- Keep capability arguments as plain DTOs; never pass domain objects that hold parent pointers.
- Run a WeakSet-based acyclicity check in dev/test before invoking capabilities.
- Replace back-references with string ids that the capability resolves server-side.
When it happens
Trigger: Calling a capability (e.g. via the generated client's capability/operation methods) and passing an argument object that references itself directly or through a chain of nested objects/arrays, e.g. `const a = {}; a.self = a;` or two objects pointing at each other.
Common situations: Attaching a parsed object back into itself (caching a node with a parent link), graph-like data structures (linked lists, tree nodes with parent pointers), or accidentally passing an object that later gets mutated to include a back-reference to an ancestor.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- AspireDict must be resolved before it can be serialized…
- AspireList must be resolved before it can be serialized…
- A circular lifetime reference was detected for resource
- Argument ' ' passed to capability ' ' is a Promise-like…
- argument ' ' passed to capability ' ' contains a circular…
AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16).
Data as JSON: /api/errors/97807227cac3a1b1.
Report an issue: GitHub.
Appendix: source
Thrown at src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.mts:427
const validateValue = (value: unknown, path: string, ancestors: Set<object>): void => {
if (value === null || value === undefined) {
return;
}
if (isPromiseLike(value)) {
throw new AppHostUsageError(
`Argument '${path}' passed to capability '${capabilityId}' is a Promise-like value. ` +
`This usually means an async builder call was not awaited. ` +
`Did you forget 'await' on a call like builder.addPostgres(...) or resource.addDatabase(...)?`
);
}
if (typeof value !== 'object') {
return;
}
if (ancestors.has(value)) {
throw createCircularReferenceError(capabilityId, path);
}
ancestors.add(value);
try {
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
validateValue(value[i], `${path}[${i}]`, ancestors);
}
return;
}
for (const [key, nestedValue] of Object.entries(value)) {
validateValue(nestedValue, `${path}.${key}`, ancestors);
}
} finally {
ancestors.delete(value);
}
};View on GitHub (pinned to 25830f84bd)