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
- Return a genuine *goja.Promise created by the same runtime instead of a custom promise-like type
- Check the %T in the message to identify the offending Go type
- Ensure the promise and the runtime used for conversion are the same instance
- 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
- Return *goja.Promise created via rt.NewPromise() from the same runtime
- Avoid custom promise-like Go structs in plugin handlers
- Never share promise objects across goja runtimes
- Check the %T in the error message to find the wrong return type quickly
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
- synchronous function returned a Promise
- failed to enable extend modules
- failed to inject global context
- globalThis.siyuan.event is not an object
- globalThis.siyuan.event is
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)