siyuan-note/siyuan · error

synchronous function returned a Promise

Error message

synchronous function returned a Promise

What it means

invokeFunction calls a plugin function and, when the call is marked synchronous (async == false), forbids the result from being a Promise/Go-future. If a synchronous invocation returns a Go Promise (isGoPromise true), it panics with this error because the caller cannot await it and the result contract is violated.

Solutions

  1. Mark the plugin function as async in its registration when the implementation returns a Promise
  2. Change the implementation to resolve the value synchronously before returning
  3. Update the API declaration to match the implementation's async behavior
  4. If wrapping JS functions, ensure sync-declared ones do not call async JS functions

Example fix

// before
// registration: {name: "getData", async: false}
func getData(vm) { return vm.RunString("fetchIt()") } // returns a Promise
// after
// registration: {name: "getData", async: true}
func getData(vm) { return vm.RunString("fetchIt()") }
Defensive patterns

Strategy: validation

Validate before calling

result := fn()
if isGoPromise(result) && !asyncSpec {
    return fmt.Errorf("function %s returns a Promise but is declared synchronous", name)
}

Type guard

func isSyncCompatible(result any) bool { return !isGoPromise(result) }

Try / catch

defer func() {
    if r := recover(); r != nil {
        callback(rt, &CallResult{Error: fmt.Errorf("sync invocation failed: %v", r)})
    }
}()

Prevention

When it happens

Trigger: Registering or invoking a plugin API function with async=false whose Go implementation returns a promise-like value (e.g. a *goja.Promise exported as a Go promise, or a function backed by an async JS handler) — detected via isGoPromise(result).

Common situations: Plugin method implemented in JS with async/await but declared synchronous in the API registry; Go wrapper returning rt.NewPromise() result for a sync API; API declaration drift between plugin manifest and implementation.

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/5485866c9e6e5bd6. Report an issue: GitHub.

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