siyuan-note/siyuan · error

'promise.then property is not a function

Error message

'promise.then property is not a function

What it means

When a plugin handler declared as async returns a thenable, the sandbox's invokeFunction inspects the returned object's 'then' property and requires it to be a callable function via goja.AssertFunction. If the resolved object is not a genuine Promise/thenable (or its 'then' was overwritten with a non-function), this error is reported to the callback instead of awaiting a result.

Solutions

  1. Inspect the plugin handler's return value and ensure it returns a real Promise (async function or new Promise) without overriding 'then'
  2. Check for plugin code that assigns to Promise.prototype.then or to a property named 'then' on the returned object
  3. Upgrade/fix the plugin; this is a contract violation of the async handler API

Example fix

// before - plugin returns a broken thenable
return { then: null };
// after
return (async () => { /* work */ })();
Defensive patterns

Strategy: try-catch

Validate before calling

const r = handler(req); if (r && typeof r.then !== 'function') throw new Error('async handler must return a Promise');

Type guard

function isThenable(v) { return v != null && typeof v.then === 'function'; }

Try / catch

try { const r = await invokeFn(); } catch (e) { if (String(e).includes('promise.then property is not a function')) { /* fix handler return value */ } else { throw e; } }

Prevention

When it happens

Trigger: A handler registered via siyuan.server[scope][requestType].handler returns a truthy object that isGoPromise classifies as a promise (i.e. Export() yields *goja.Promise), but resultObj.Get("then") is not callable - e.g. a thenable whose 'then' property is a value, null, or was overwritten by plugin code.

Common situations: Plugin code mutates the returned Promise (assigning then = something), returns a custom thenable-like object wrapped incorrectly, or a Go-side object leaks into the return path where 'then' is data rather than a method. Rare; usually indicates a plugin bug corrupting the Promise prototype or shadowing 'then'.

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/84f30f9707abab92. Report an issue: GitHub.

Appendix: source

Thrown at kernel/plugin/sandbox.go:394

		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})
			}
		}), rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) {
			callback(rt, &CallResult{Error: fmt.Errorf("promise rejected: %v", call.Argument(0).Export())})
		}))
	} else {
		callback(rt, &CallResult{Value: resultJs})
	}
}

View on GitHub (pinned to 9f775e8a12)