Thenable Object
ID |
javascript.thenable_object |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
async, naming, reliability |
Description
Reports an object literal or a class that declares a callable then member, which silently turns the value into a promise as far as the language is concerned.
Only a then whose value is a function is reported: the specification calls the member only when it is callable, so {then: 'tomorrow'} is an ordinary property and not a thenable. A static then() is not reported either — the machinery reads the member off the value it is adopting, and that is an instance.
Rationale
// Bad
const step = {
name: 'deploy',
then(next) {
return this.chain(next);
}
};
const done = await run(); // `done` is whatever `then` passed on, never `step`
-
The promise machinery does not check types, it checks for a member.
await,Promise.resolveand every.thenin a chain look for a property namedthenand, if it is callable, hand it a resolve and a reject function and adopt whatever it produces. -
An object that declares
thenfor its own reasons — a builder, a pipeline step, a query object, a state machine — is therefore assimilated the moment it reaches any of them. Returning it from anasyncfunction is enough, since the return value is resolved. -
Nothing reports an error.
await objyields the valueobj.thenpassed to its callback instead ofobj, and ifthennever calls back theawaithangs for ever with no stack to look at.
Remediation
Rename the member. then belongs to the promise protocol; any other verb keeps the object an ordinary value.
// Good
const step = {
name: 'deploy',
andThen(next) {
return this.chain(next);
}
};
// Good — if the object really is a promise, say so by returning one
class Job {
run() {
return this.queue.enqueue(this); // a real Promise, not a look-alike
}
}