siyuan-note/siyuan · error

expected promise object, got %T

Error message

expected promise object, got %T

What it means

After isGoPromise confirmed the result is a *goja.Promise, resultJs.ToObject(rt) returned nil — the promise value could not be turned into a goja.Object to read its .then property. Indicates a malformed/foreign thenable that Exported as a Promise type but is not object-convertible in this runtime.

Source

Thrown at kernel/plugin/sandbox.go:387

func invokeFunction(callback func(rt *goja.Runtime, result *CallResult), rt *goja.Runtime, async bool, fn goja.Callable, this goja.Value, args ...goja.Value) {
	resultJs, invokeErr := fn(this, args...)
	if callback == nil {
		return
	}

	if invokeErr != nil {
		callback(rt, &CallResult{Error: invokeErr})
		return
	}

	result := resultJs.Export()
	if isGoPromise(result) {
		if !async {
			panic(fmt.Errorf("synchronous function returned a Promise"))
		}
		resultObj := resultJs.ToObject(rt)
		if resultObj == nil {
			callback(rt, &CallResult{Error: fmt.Errorf("expected promise object, got %T", result)})
			return
		}

		thenValue := resultObj.Get("then")
		then, ok := goja.AssertFunction(thenValue)
		if !ok {
			callback(rt, &CallResult{Error: fmt.Errorf("'promise.then property is not a function")})
			return
		}

		then(resultObj, rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) {
			// ⚠️ call.Arguments always is an empty array.
			promise, ok := result.(*goja.Promise)
			if ok {
				callback(rt, &CallResult{Value: promise.Result()})
			} else {
				callback(rt, &CallResult{Value: resultJs})
			}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Ensure the handler returns a Promise created within the same goja.Runtime (use the runtime's own Promise constructor, not an imported polyfill).
  2. Avoid returning wrapped/proxy thenables; return a value the runtime recognizes as its native Promise.
  3. If bridging runtimes, resolve the promise in the source runtime and pass the resolved value instead of the Promise object.

Example fix

// before (Promise from a polyfill)
globalThis.siyuan.server.network.beforeRequest.handler = () => PromisePolyfill.resolve(1)
// after
const P = globalThis.Promise // runtime-native
globalThis.siyuan.server.network.beforeRequest.handler = () => P.resolve(1)
Defensive patterns

Strategy: type-guard

Validate before calling

// Plugin side: only return Promises built from the runtime-native constructor.
const NativePromise = globalThis.Promise
handler = () => NativePromise.resolve(1)

Type guard

function isNativePromise(v: unknown): boolean {
  return v instanceof globalThis.Promise
}

Prevention

When it happens

Trigger: A plugin returns a value that goja's Export reports as *goja.Promise but which is a cross-runtime or proxy-wrapped thenable; invoking a Promise from a different goja.Runtime instance; a thenable constructed via Object.create/Proxy that defeats ToObject.

Common situations: Plugin shares a Promise created in another runtime/worker; plugin uses a polyfilled Promise shim that is not the runtime's native Promise; corrupted goja version mismatch.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/98af6f09c3a51013. Report an issue: GitHub.