dotnet/runtime · error · Error

NotImplementedException ${jsType}. ${jsinteropDoc}

Error message

NotImplementedException ${jsType}. ${jsinteropDoc}

What it means

Thrown by marshalCsObjectToCs when a value carries the jsOwnedGcHandleSymbol (so it is treated as a C#-roundtripped object) but its runtime type is neither ManagedError, ManagedObject, nor ArraySegment. The .NET JS interop marshaler only knows how to send these specific managed proxy shapes back to C#, so any other prototype that happens to carry the gcHandle symbol is rejected as not implemented. The message includes the JS typeof string and a link to the jsinterop docs.

Source

Thrown at src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/marshal-to-cs.ts:414

                if (BuildConfiguration === "Debug" && Object.isExtensible(value)) {
                    value[proxyDebugSymbol] = `JS Object with JSHandle ${jsHandle}`;
                }
                setJsHandle(arg, jsHandle);
            } else {
                throw new Error(`JSObject proxy is not supported for ${jsType} ${value}`);
            }
        } else {
            assertNotDisposed(value);
            if (value instanceof ArraySegment) {
                throw new Error("NotImplementedException: ArraySegment. " + jsinteropDoc);
            } else if (value instanceof ManagedError) {
                setArgType(arg, MarshalerType.Exception);
                setGcHandle(arg, gcHandle);
            } else if (value instanceof ManagedObject) {
                setArgType(arg, MarshalerType.Object);
                setGcHandle(arg, gcHandle);
            } else {
                throw new Error("NotImplementedException " + jsType + ". " + jsinteropDoc);
            }
        }
    }
}

export function marshalArrayToCs(arg: JSMarshalerArgument, value: Array<any> | TypedArray | undefined | null, elementType?: MarshalerType): void {
    dotnetAssert.check(!!elementType, "Expected valid elementType parameter");
    marshalArrayToCsImpl(arg, value, elementType);
}

export function marshalArrayToCsImpl(arg: JSMarshalerArgument, value: Array<any> | TypedArray | undefined | null, elementType: MarshalerType): void {
    if (value === null || value === undefined) {
        setArgType(arg, MarshalerType.None);
    } else {
        const elementSize = arrayElementSize(elementType);
        dotnetAssert.fastCheck(elementSize != -1, () => `Element type ${elementType} not supported`);
        const length = value.length;
        const bufferLength = elementSize * length;

View on GitHub (pinned to 60108ba66e)

Solutions

  1. Pass the original ManagedObject/ManagedError instance directly instead of a wrapper or copy; do not transplant the gcHandle symbol onto other objects.
  2. If you subclassed ManagedObject, ensure instanceof checks still hold (do not override Symbol.hasInstance / prototype chains) or unwrap to the base instance before the interop call.
  3. Rebuild and redeploy the dotnet wasm runtime and JS loader from the same source/commit so the ManagedObject class identity used by instanceof matches at runtime.
  4. Audit the call site with a debugger break at marshal-to-cs.ts:414 to confirm the value's prototype and that value[jsOwnedGcHandleSymbol] is genuinely expected.

Example fix

// before
const wrapped = Object.assign({}, csharpObject); // carries jsOwnedGcHandleSymbol via copy?
exportToCSharp(wrapped); // throws NotImplementedException object.

// after
exportToCSharp(csharpObject); // pass the original ManagedObject instance
Defensive patterns

Strategy: type-guard

Validate before calling

import { ManagedObject, ManagedError, ArraySegment } from 'System.Runtime.InteropServices.JavaScript.Native/interop/core';

function isMarshalableCsObject(v: unknown): boolean {
  if (v === null || v === undefined) return true; // None path
  if (!(typeof v === 'object')) return false;
  const hasGc = (v as any)[jsOwnedGcHandleSymbol] !== undefined;
  if (!hasGc) return true; // non-gcHandle branch handles primitives/arrays/etc.
  return v instanceof ManagedObject || v instanceof ManagedError || v instanceof ArraySegment;
}

if (!isMarshalableCsObject(arg)) throw new TypeError('arg is not a marshalable managed proxy');

Type guard

function isManagedObjectLike(v: unknown): v is ManagedObject | ManagedError | ArraySegment {
  return v instanceof ManagedObject || v instanceof ManagedError || v instanceof ArraySegment;
}

Try / catch

try {
  exportToCSharp(maybeProxy);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('NotImplementedException ')) {
    throw new TypeError('Passed a non-managed object to C# interop; unwrap to the original ManagedObject.', { cause: e });
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a value to a C# [JSExport] argument where the value has the jsOwnedGcHandleSymbol property but is an instance of a custom/extension class rather than ManagedObject/ManagedError/ArraySegment (e.g. an object that wrapped or cloned a managed proxy, or a ManagedObject subclass the marshaler does not recognize). Reached whenever marshalCsObjectToCs takes the gcHandle !== undefined branch at marshal-to-cs.ts:403 and falls through all instanceof checks to line 414.

Common situations: Manually copying properties off a ManagedObject onto a plain object (which then inherits/retains the symbol), subclassing ManagedObject in user JS interop code, passing a stale proxy whose prototype was swapped, or using reflection-like wrappers around C# objects. Also seen when a NuGet dotnet wasm build is mismatched with the JS runtime so proxy class identity checks (instanceof ManagedObject) fail.

Related errors


AI-assisted analysis of dotnet/runtime@60108ba66e (2026-08-10). Data as JSON: /api/errors/3f3747874adcb224. Report an issue: GitHub.