siyuan-note/siyuan · error

expected promise object, got %T

Error message

expected promise object, got %T

What it means

In invokeFunction's async path, a Go-promise result must be convertible to a JS object exposing .then. If resultJs.ToObject(rt) returns nil, the runtime cannot wrap the promise, so the callback receives this error describing the actual Go type.

Solutions

  1. Return a genuine *goja.Promise created by the same runtime instead of a custom promise-like type
  2. Check the %T in the message to identify the offending Go type
  3. Ensure the promise and the runtime used for conversion are the same instance
  4. Verify the runtime is not interrupted at conversion time

Example fix

// before
type myPromise struct{ done chan struct{} }
return &myPromise{} // not a real goja promise
// after
promise, resolve, reject := rt.NewPromise()
return promise
Defensive patterns

Strategy: type-guard

Validate before calling

result := fn()
if isGoPromise(result) {
    if _, ok := result.(*goja.Promise); !ok {
        return fmt.Errorf("must return *goja.Promise, got %T", result)
    }
}

Type guard

func isRealPromise(rt *goja.Runtime, result any) bool {
    p, ok := result.(*goja.Promise)
    return ok && p != nil
}

Try / catch

resultObj := resultJs.ToObject(rt)
if resultObj == nil {
    callback(rt, &CallResult{Error: fmt.Errorf("unconvertible promise result")})
    return
}

Prevention

When it happens

Trigger: Async plugin invocation whose exported result passes isGoPromise but fails goja's ToObject conversion — e.g. a custom Go type that merely resembles a promise but has no JS representation in this runtime, or the runtime is in a broken state.

Common situations: Custom promise-like Go types returned from plugin handlers; mixing runtimes (promise created in runtime A, awaited in runtime B); runtime interrupted between export and conversion.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/98af6f09c3a51013. Report an issue: GitHub.

Appendix: 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 9f775e8a12)