{"record":{"id":"4116e244096d402b","repo":"GopeedLab/gopeed","slug":"promise-then-is-not-callable","errorCode":null,"errorMessage":"promise.then is not callable","messagePattern":"promise\\.then is not callable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/download/engine/engine.go","lineNumber":107,"sourceCode":"\t\tvalue, err := fn(runtime)\n\t\tif err != nil {\n\t\t\tsendResult(result{err: err})\n\t\t\treturn\n\t\t}\n\t\tif p, ok := value.Export().(*goja.Promise); ok {\n\t\t\tswitch p.State() {\n\t\t\tcase goja.PromiseStateFulfilled:\n\t\t\t\tsendResult(result{value: exportJSValue(p.Result())})\n\t\t\t\treturn\n\t\t\tcase goja.PromiseStateRejected:\n\t\t\t\tsendResult(result{err: exportJSError(p.Result())})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tpromiseObj := value.ToObject(runtime)\n\t\t\tthenVal := promiseObj.Get(\"then\")\n\t\t\tthenFn, ok := goja.AssertFunction(thenVal)\n\t\t\tif !ok {\n\t\t\t\tsendResult(result{err: errors.New(\"promise.then is not callable\")})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tonFulfilled := runtime.ToValue(func(call goja.FunctionCall) goja.Value {\n\t\t\t\tsendResult(result{value: exportJSValue(call.Argument(0))})\n\t\t\t\treturn goja.Undefined()\n\t\t\t})\n\t\t\tonRejected := runtime.ToValue(func(call goja.FunctionCall) goja.Value {\n\t\t\t\tsendResult(result{err: exportJSError(call.Argument(0))})\n\t\t\t\treturn goja.Undefined()\n\t\t\t})\n\t\t\tif _, err := thenFn(promiseObj, onFulfilled, onRejected); err != nil {\n\t\t\t\tsendResult(result{err: err})\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tsendResult(result{value: exportJSValue(value)})\n\t})\n\tif !ok {","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/GopeedLab/gopeed/blob/7b7327ffb30816273a74b142cccc0bc10c5a4c67/pkg/download/engine/engine.go#L89-L125","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (script)\nresolve: { then: null, url: 'http://x' } // then not callable\n\n// after (script)\nresolve(Promise.resolve({ url: 'http://x' }))\n// or\nreturn (async () => ({ url: 'http://x' }))()","handlingStrategy":"try-catch","validationCode":"// Script-side: always return a real promise so `then` is callable\n// return Promise.resolve(value) or an async function's result","typeGuard":"// Script-side self-check before returning:\nconst v = makeResult()\nif (v != null && typeof v.then !== 'function' && !(v instanceof Promise)) {\n    return Promise.resolve(v)\n}\nreturn v","tryCatchPattern":"v, err := engine.RunString(script)\nif err != nil {\n    if strings.Contains(err.Error(), \"promise.then is not callable\") {\n        // script returned a broken thenable; fix script to return a real Promise\n    }\n}","preventionTips":["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"],"tags":["javascript","goja","promise","engine"],"backgroundTag":null,"analyzedSha":"7b7327ffb30816273a74b142cccc0bc10c5a4c67","analyzedAt":"2026-08-16T02:51:03.250Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}