Skip to content

Polyglot TypeScript AppHost: confusing error when missing 'await' on async builder calls #14724

@joperezr

Description

@joperezr

Description

When writing a TypeScript AppHost, if the developer forgets an await on an async builder call (e.g., addPostgres), the error surfaced during aspire run is a confusing internal .NET error rather than a helpful message pointing to the TypeScript coding mistake.

Steps to Reproduce

  1. Create a TypeScript AppHost with the following apphost.ts:
const postgres = builder.addPostgres("postgres");
const counterdb = postgres.addDatabase("counterdb");

const frontend = builder.addNodeApp("frontend", "./frontend", "server.js")
  .withNpm()
  .withHttpEndpoint({ port: 3000, env: "PORT" })
  .withExternalHttpEndpoints()
  .withReference(counterdb)
  .waitFor(counterdb);
  1. Run aspire run

Actual Behavior

The following error is displayed:

❌ Capability Error: The service collection cannot be modified because it is read-only. Code: INTERNAL_ERROR Capability: Aspire.Hosting.PostgreSQL/addDatabase ❌ An unexpected error occurred: The TypeScript (Node.js) apphost failed. 📄 See logs at C:\Users\joperezr\.aspire\logs\cli_20260226T053154_dfc30a18.log

This error is confusing because:

  • It references an internal .NET concept ("service collection is read-only") that has no meaning to a TypeScript developer
  • The error code INTERNAL_ERROR gives no actionable guidance
  • There is no indication that the root cause is a missing await in the user's TypeScript code

Expected Behavior

The error should be more actionable. Ideally:

  • Detect that the TypeScript process exited with an error and surface it as a compilation/runtime error in the guest AppHost
  • Provide guidance like: "Your apphost.ts encountered an error. Check that async calls like addPostgres() and addDatabase() are properly awaited."
  • Or at minimum, surface the underlying TypeScript/Node.js error output clearly rather than the .NET-side capability error

Root Cause Analysis

The issue is that addPostgres() returns a PostgresResourceBuilderPromise (thenable). Calling .addDatabase() on it chains correctly via the Promise thenable pattern, but the result (counterdb) is still a Promise that hasn't been resolved yet. When this unresolved promise is passed to .withReference(counterdb), it gets sent over JSON-RPC as an invalid argument, causing the .NET server to fail with an internal error.

The correct code requires await:

const postgres = await builder.addPostgres("postgres");
const counterdb = await postgres.addDatabase("counterdb");

Suggestion

Consider one or more of these improvements:

  1. Client-side validation: The generated SDK could validate that arguments passed to capability methods are resolved handles, not pending promises, and throw a clear TypeScript error like "Cannot pass an unresolved promise as an argument. Did you forget 'await'?"
  2. Server-side error mapping: When the server receives an invalid argument type, map it to a user-friendly error rather than exposing the internal .NET exception
  3. Error output forwarding: When the guest AppHost process fails, capture and display its stderr/stdout prominently instead of (or in addition to) the RPC error

Metadata

Metadata

Assignees

Labels

area-app-modelIssues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplicationarea-polyglotIssues related to polyglot apphosts

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions