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

  1. Make the handler synchronous: drop async and await, return a plain value.
  2. If async work is required, register the handler through the async-aware registration path so invokeFunction receives async==true.
  3. 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

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


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