dotnet/aspnetcore · error · Error

There is no pending async call with ID

Error message

There is no pending async call with ID ${asyncCallId}.

What it means

Thrown by CallDispatcher.completePendingCall() when .NET reports completion for an asyncCallId that is not in the _pendingAsyncCalls map. Each async JS->.NET call registers an ID at dispatch time; receiving a completion for an unknown ID means the call was never started, already completed, or the ID is corrupt.

Solutions

  1. Ensure the Microsoft.JSInterop.JS npm package / static asset version matches the .NET runtime version.
  2. Avoid calling the internal endInvokeDotNetFromJS / completePendingCall APIs directly; let Blazor manage them.
  3. After a Blazor reconnect, re-issue interop calls rather than relying on stale IDs.

Example fix

// before (manual misuse)
dispatcher.endInvokeDotNetFromJS('999', true, '{}'); // id 999 never started

// after
// only the runtime should call endInvokeDotNetFromJS; use the public
// invokeMethodAsync promise to receive results.
Defensive patterns

Strategy: try-catch

Try / catch

try { return await dispatcher.invokeDotNetStaticMethodAsync('MyLib', 'Method'); } catch (e) { if (/no pending async call/.test(e.message)) { console.error('Stale or unknown async call id — possible version skew or reconnect replay.'); } throw e; }

Prevention

When it happens

Trigger: The .NET side calls endInvokeDotNetFromJS with a callId that doesn't match any beginInvokeDotNetFromJS call; double-completion (the same call ID resolved twice); the call entry was already deleted by a prior completion. Typically indicates a runtime/protocol mismatch.

Common situations: Version skew between the JS interop library and the .NET runtime (ID format changed); a hot-reload or signal-reconnect replaying old completions; manual misuse of the internal dispatcher API.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/605ba3e6ce50e61a. Report an issue: GitHub.

Appendix: source

Thrown at src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts:550

          // it's not something the developer gets to control, and it would be an error if it doesn't.
          let result: Promise<ReadableStream>;
          if (this._pendingDotNetToJSStreams.has(streamId)) {
              // We've already started receiving the stream, so no longer need to track it as pending
              result = this._pendingDotNetToJSStreams.get(streamId)!.streamPromise!;
              this._pendingDotNetToJSStreams.delete(streamId);
          } else {
              // We haven't started receiving it yet, so add an entry to track it as pending
              const pendingStream = new PendingStream();
              this._pendingDotNetToJSStreams.set(streamId, pendingStream);
              result = pendingStream.streamPromise;
          }

          return result;
      }

      private completePendingCall(asyncCallId: number, success: boolean, resultOrError: any) {
          if (!this._pendingAsyncCalls.hasOwnProperty(asyncCallId)) {
              throw new Error(`There is no pending async call with ID ${asyncCallId}.`);
          }

          const asyncCall = this._pendingAsyncCalls[asyncCallId];
          delete this._pendingAsyncCalls[asyncCallId];
          if (success) {
              asyncCall.resolve(resultOrError);
          } else {
              asyncCall.reject(resultOrError);
          }
      }
  }

  function formatError(error: Error | string): string {
      if (error instanceof Error) {
          return `${error.message}\n${error.stack}`;
      }

      return error ? error.toString() : "null";

View on GitHub (pinned to 3600ca084e)