cljs: settling a pending promise runs its callbacks inside resolve(), so a 2000-link then chain overflows the stack (Promises/A+ 2.2.4)
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 54/100
Direzione di ricerca
Start in src/promesa/impl/promise.js and trace how transition drains callbacks compared with how process schedules callbacks for an already-settled promise. Reproduce the pending-promise behavior using the provided Node script, then check the project’s promise tests for coverage of callback ordering and long chains. Done when resolving a pending promise defers callbacks as required by Promises/A+ 2.2.4 and long chains settle without overflowing the stack.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
In the ClojureScript implementation (src/promesa/impl/promise.js), settling a pending promise runs its queued callbacks synchronously, inside resolve() / reject(): transition calls processNextTick(p) directly. Adding a callback to an already-settled promise is deferred (process goes through nextTick), so only the "pending, then settled" path is affected.
Promises/A+ 2.2.4 says onFulfilled or onRejected must not be called until the execution context stack contains only platform code, and native Promise behaves that way.
Because each link settles inside the frame of the link before it:
- a chain of 2000 callbacks on a pending promise overflows the stack when the promise is resolved; 1000 still settles (Node 24.14.0, default stack size);
- the
RangeErroris thrown out ofresolve()into the caller, and the rest of the chain stays pending; - code after
resolve()runs after callbacks that A+ would run later.
From ClojureScript this is (p/deferred), a chain of p/then, then (p/resolve d v). Those call the same deferred, .then and .resolve as the script below.
Reproduction
Plain JS against promesa/impl/promise.js from the 12.0.1 jar (identical to master at 9b38ac3), loaded with a minimal goog shim:
// node repro.js path/to/promesa/impl/promise.js
const fs = require("fs"), vm = require("vm");
let uid = 0;
globalThis.goog = {
global: globalThis,
provide(ns) { let o = globalThis; for (const k of ns.split(".")) o = (o[k] ??= {}); },
scope(f) { f.call(globalThis); },
isObject(v) { const t = typeof v; return (t === "object" && v !== null) || t === "function"; },
getUid(o) { return (o.__uid ??= ++uid); },
};
vm.runInThisContext(fs.readFileSync(process.argv[2], "utf8"));
const P = promesa.impl.promise;
const promesaPending = () => { const d = P.deferred(); return [d, (v) => d.resolve(v)]; };
const nativePending = () => { let r; const p = new Promise((res) => (r = res)); return [p, r]; };
const tick = () => new Promise((r) => setTimeout(r, 50));
// 1. Order: does a callback run inside resolve()?
async function order(pending) {
const [p, resolve] = pending(), log = [];
p.then(() => log.push("callback"));
resolve(1);
log.push("resolve() returned");
await tick();
return JSON.stringify(log);
}
// 2. Depth: N callbacks chained on a pending promise, then resolved.
async function chain(N, pending) {
const [head, resolve] = pending();
let tail = head, outcome = "still pending";
for (let i = 0; i < N; i++) tail = tail.then((x) => x + 1);
tail.then((v) => (outcome = `settled ${v}`), (e) => (outcome = `rejected: ${e.message}`));
let threw = "";
try { resolve(0); } catch (e) { threw = `resolve() threw ${e.name}, `; }
await tick();
return threw + outcome;
}
(async () => {
console.log(`order promesa: ${await order(promesaPending)} native: ${await order(nativePending)}`);
for (const N of [1000, 2000, 20000])
console.log(`N=${N} promesa: ${await chain(N, promesaPending)} native: ${await chain(N, nativePending)}`);
})();
node repro.js promesa/impl/promise.js on Node 24.14.0:
order promesa: ["callback","resolve() returned"] native: ["resolve() returned","callback"]
N=1000 promesa: settled 1000 native: settled 1000
N=2000 promesa: resolve() threw RangeError, still pending native: settled 2000
N=20000 promesa: resolve() threw RangeError, still pending native: settled 20000
A possible fix
Defer in transition the way process already does:
p[STATE] = state;
p[VALUE] = value;
- return processNextTick(p);
+ nextTick(processNextTick, p);
}
With only that change, the same script prints ["resolve() returned","callback"] and settles all three chains. It adds a microtask per settled promise, and I haven't run promesa's own test suite with it. If running callbacks in place is intended (then's docstring says they run "in the completion thread by default"), deferring only past some nesting depth would keep that and still bound the stack.
We hit this as RangeErrors in a cljc project on Node, and now avoid long chains in our own code. #172 is a separate bug in the same drain loop.
— commented by mydeck bot (mydeck:d0dea098)
- Lingua principale
- Clojure
- Stelle
- 547
- Fork
- 66
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di funcool/promesa
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
-
A throwing task corrupts other tasks in the same processNextTick drain (wrong value in 12.0.1, wrong rejection in 11.0.678)Forse già presa @yqrashawn l’ha presa 12 giorni fa. Aperta
Difficoltà 3/5 Mezza giornata Idoneità per principianti 70/100
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 76/100
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 52/100
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 25/100
Tutte le issue di funcool/promesa
Issue simili
-
bug triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
.Team/UXWest Misc/Accessibility Priority:P3 Reporting/Dashboards Type:Bug Visualizer
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
metabase/metabase#86780 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
nextjournal/markdown#77 ·
-
Expose the ECA version to hook payloadsForse già presa @zikajk l’ha presa 1 giorno fa. Aperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
editor-code-assistant/eca#697 · 1 assegnatario ·
I maintainer di solito rispondono entro 2 giorni
-
enhancement good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
itsOmidKarami/kraft#1346 ·
I maintainer di solito rispondono entro 1 giorno