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

  1. Remove the cycle: drop parent/back pointers or replace them with ids before passing the argument.
  2. Send a plain projection of the data (structuredClone won't work on cycles; build a shallow DTO with only needed fields).
  3. Pass JSON.parse(JSON.stringify()) of a cycle-free version, or encode cyclic links as string ids the capability resolves server-side.
  4. 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

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


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)