You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
await thenable returns the thenable itself instead of calling .then(resolve, reject) #586
Perry's await only handles real Promises — it doesn't recognize objects with a .then(resolve, reject) method (the thenable protocol). When you await a thenable, perry returns the thenable object itself unchanged.
Spec
ECMAScript await semantics: if the awaited value is an object with a callable then property, it should be assimilated — perry should call value.then(resolve, reject) and resume the surrounding async function with whatever resolve is called with. Both real Promise instances and user-defined thenables (Q, Bluebird, Drizzle's QueryPromise, etc.) follow this contract.
drizzle-orm's QueryPromise (drizzle-orm/query-promise.js) is the load-bearing pattern:
classQueryPromise{static[entityKind]="QueryPromise";[Symbol.toStringTag]="QueryPromise";then(onFulfilled,onRejected){returnthis.execute().then(onFulfilled,onRejected);// executes lazily on first await}}
db.select().from(users) returns a PgSelectBase extending QueryPromise. The intent is await db.select().from(users) calls .then(resolve), which triggers .execute() (the actual SQL round-trip), and resolves with rows. Without thenable assimilation, await returns the builder object unchanged — query never runs.
This is the second perry-side blocker for #488 (after #585 cross-module instanceof).
Where I'd start digging
crates/perry-runtime/src/promise.rs (await/resolve path) and the async-to-generator transform in crates/perry-transform/src/generator.rs. The relevant point is wherever the await is evaluated — when the awaited value is not a Promise, perry should:
Probe for a .then method via js_object_get_field_by_name(value, "then")
If callable, allocate a fresh Promise, call value.then(resolve, reject) where resolve/reject route to the new promise
Suspend the async function on that new Promise
The fast path for real Promises stays unchanged.
Workaround
User-side await Promise.resolve(thenable.execute()) works (bypasses the await-via-thenable assimilation by calling .execute() directly and awaiting the result). For drizzle this would mean rewriting every query call site to call .execute() explicitly — invasive enough that fixing the runtime is the right move.
Refs
Blocks #488. Surfaced from #420 sub-issue acceptance run on v0.5.684.
Fixed in 1fd8598 (v0.5.692). Thenable assimilation in js_promise_resolved (since the async-to-generator transform routes await x through Promise.resolve(x).then(...)); plus belt-and-suspenders in codegen Expr::Await for un-transformed paths.
Activity
fix(runtime+codegen): closes #585 #586 — cross-module instanceof + aw…
proggeramlug commentedon May 8, 2026
Fixed in 1fd8598 (v0.5.692). Thenable assimilation in
js_promise_resolved(since the async-to-generator transform routesawait xthroughPromise.resolve(x).then(...)); plus belt-and-suspenders in codegenExpr::Awaitfor un-transformed paths.