siyuan-note/siyuan · critical
synchronous function returned a Promise
Error message
synchronous function returned a Promise
What it means
A plugin handler registered as synchronous returned a JavaScript Promise. invokeFunction detected isGoPromise(result)==true while async==false, and panics because the synchronous call contract cannot await. This is a contract violation by the plugin, not a transient runtime fault.
Source
Thrown at kernel/plugin/sandbox.go:383
return
}
// invokeFunction calls a goja.Callable with the given this and arguments, handling both synchronous return values and Promises.
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 {View on GitHub (pinned to 251596fc0d)
Solutions
- Make the handler synchronous: drop async and await, return a plain value.
- If async work is required, register the handler through the async-aware registration path so invokeFunction receives async==true.
- Verify the handler does not implicitly return a Promise via an arrow body calling a Promise-returning helper.
Example fix
// before
globalThis.siyuan.event.on = async (e) => { return await compute(e) }
// after
globalThis.siyuan.event.on = (e) => { return compute(e) } // sync, returns value not Promise Defensive patterns
Strategy: validation
Validate before calling
// Plugin side: refuse to register an async function for a sync hook.
function registerSyncHook(path: string[], fn: Function): void {
if (fn.constructor.name === 'AsyncFunction') {
throw new Error(`refusing async fn for sync hook ${path.join('.')}`)
}
// ...assign fn
} Type guard
function isAsyncFunction(fn: unknown): fn is (...a: any[]) => Promise<any> {
return typeof fn === 'function' && (fn as any).constructor.name === 'AsyncFunction'
} Prevention
- Keep synchronous hooks synchronous: no async, no await, no Promise return.
- When refactoring a sync handler to async, switch its registration to the async-aware path.
- Audit arrow bodies that call Promise-returning helpers — they implicitly return a Promise.
When it happens
Trigger: A plugin registers a synchronous hook (event.on or server[scope][requestType].handler invoked in sync mode) whose implementation returns a Promise (uses async function or returns fetch/thenable). The kernel's sync invocation path has no way to await it.
Common situations: Plugin author wrote an async function for a hook the kernel calls synchronously; refactored a sync handler to async without updating the registration flag; mixing await-based helpers into a sync hook.
Related errors
- promise rejected: %v
- path %v: value is nil
- path %v: value is %s
- path %v: expected object, got %T
- expected promise object, got %T
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/5485866c9e6e5bd6.
Report an issue: GitHub.