GopeedLab/gopeed · error
promise.then is not callable
Error message
promise.then is not callable
What it means
In Engine.runOnLoop (pkg/download/engine/engine.go:94-121), when a script's return value exports as a *goja.Promise that is neither fulfilled nor rejected, the engine attaches to it via promise.then. If the property 'then' of the value is missing or not callable (goja.AssertFunction fails), the run fails with 'promise.then is not callable'. This means the script returned a pending promise-like object whose then is not a function — effectively a broken thenable.
Source
Thrown at pkg/download/engine/engine.go:107
value, err := fn(runtime)
if err != nil {
sendResult(result{err: err})
return
}
if p, ok := value.Export().(*goja.Promise); ok {
switch p.State() {
case goja.PromiseStateFulfilled:
sendResult(result{value: exportJSValue(p.Result())})
return
case goja.PromiseStateRejected:
sendResult(result{err: exportJSError(p.Result())})
return
}
promiseObj := value.ToObject(runtime)
thenVal := promiseObj.Get("then")
thenFn, ok := goja.AssertFunction(thenVal)
if !ok {
sendResult(result{err: errors.New("promise.then is not callable")})
return
}
onFulfilled := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
sendResult(result{value: exportJSValue(call.Argument(0))})
return goja.Undefined()
})
onRejected := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
sendResult(result{err: exportJSError(call.Argument(0))})
return goja.Undefined()
})
if _, err := thenFn(promiseObj, onFulfilled, onRejected); err != nil {
sendResult(result{err: err})
}
return
}
sendResult(result{value: exportJSValue(value)})
})
if !ok {View on GitHub (pinned to 7b7327ffb3)
Solutions
- Return a real promise from the script (async function, Promise.resolve(...), or the engine's own resolve helper) instead of a hand-made thenable
- If returning an object, ensure it either is a promise or has then: function(onF, onR){...}
- Inspect what the script actually returns with a small probe (run a modified script returning JSON.stringify(result)) to find the malformed value
- For extension authors: prefer resolve(res) style API over returning values
Example fix
// before (script)
resolve: { then: null, url: 'http://x' } // then not callable
// after (script)
resolve(Promise.resolve({ url: 'http://x' }))
// or
return (async () => ({ url: 'http://x' }))() Defensive patterns
Strategy: try-catch
Validate before calling
// Script-side: always return a real promise so `then` is callable // return Promise.resolve(value) or an async function's result
Type guard
// Script-side self-check before returning:
const v = makeResult()
if (v != null && typeof v.then !== 'function' && !(v instanceof Promise)) {
return Promise.resolve(v)
}
return v Try / catch
v, err := engine.RunString(script)
if err != nil {
if strings.Contains(err.Error(), "promise.then is not callable") {
// script returned a broken thenable; fix script to return a real Promise
}
} Prevention
- Return real promises (async functions / Promise.resolve) from scripts
- Avoid custom thenables in scripts run by this engine
- Don't override or shim Promise with a non-standard implementation
When it happens
Trigger: A resolve/extension script returns an object like {then: 42} or {then: 'x'} instead of a real promise; a user-supplied script returns a custom thenable whose then field is undefined because of a typo or wrong JSON field mapping (the runtime uses TagFieldNameMapper("json", true), so Go structs exported to JS expose json-tagged names); a script returns a promise subclass instance whose prototype chain was mutated.
Common situations: Hand-written resolver scripts returning an ad-hoc object instead of calling the built-in resolve()/Promise helpers; scripts written for another engine (Node) that return classes/thenables goja cannot introspect; field-name mismatches after renaming struct json tags.
Related errors
AI-assisted analysis of GopeedLab/gopeed@7b7327ffb3 (2026-08-16).
Data as JSON: /api/errors/4116e244096d402b.
Report an issue: GitHub.