processNextTick leaks failure state between callbacks queued on the same promise in one tick

Aperta Adatta ai principianti
#176 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
85/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
clojure, javascript
Ambito
backend, testing

Direzione di ricerca

Inizia in src/promesa/impl/promise.js, in corrispondenza di processNextTick, e confronta la gestione della coda descritta per le versioni 11.0.678 e 12.0.1. Mantieni indipendente lo stato dell’esito di ogni attività accodata, quindi esegui o aggiungi un caso di regressione basato sulla riproduzione: un callback che genera un’eccezione, seguito da un callback riuscito sulla stessa promise, dovrebbe lasciare risolta la seconda chain.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Summary

In promesa.impl.promise/processNextTick, the variable holding a task's outcome is declared outside the while loop and never reset between iterations. When one task throws, tasks after it in the same queue are completed as though they had failed too, even though their callbacks returned normally.

Callbacks attached to the same promise are independent of each other. One throwing should not affect the others.

Reproduction

(require '[promesa.core :as p])

(let [pr (p/resolved 1)]
  ;; both handlers are attached before the tick runs, so they share one queue
  (p/catch (p/then pr (fn [_] (throw (ex-info "boom" {}))))
           (fn [e] (println "first  rejected:" (ex-message e))))
  (p/catch (p/then pr (fn [v] (println "second resolved:" v)))
           (fn [e] (println "second rejected:" (ex-message e)))))

Expected:

first  rejected: boom
second resolved: 1

Actual, 11.0.678:

first  rejected: boom
second resolved: 1
second rejected: boom

The second callback runs, but the promise it returns is rejected with the first callback's exception.

Actual, 12.0.1:

first  rejected: boom
second rejected: nil

The second callback does not run at all, and its catch receives the promise's resolved value 1 as the cause, hence the nil from ex-message.

(Both built with ClojureScript 1.11.132, :target :nodejs, :optimizations :none, Node 22.)

Order matters: attach the throwing handler second and both behave correctly, which makes this easy to miss.

Cause

Same defect in both versions, different variable.

11.0.678, src/promesa/impl/promise.js#L269: let task, rvalue, rcause; is outside the loop. The catch sets rcause = e, and the next iteration reaches resolveTask(task, rvalue, rcause) with that error still in rcause, so if (cause) rejects a task that succeeded.

master / 12.0.1: rcause is gone, but let state = p[STATE] is now hoisted the same way and set to REJECTED in the catch. Later iterations then take the state === REJECTED branch and call task.reject(value) with the promise's resolved value, and resolveTask completes them as rejected.

Impact

Callbacks on a shared promise cannot be treated as independent, and the failure is misattributed: the error surfaced on the second chain was raised by unrelated code, or on 12.0.1 is not an error object at all. We hit this in Logseq where a batch of asset downloads chained off one promise, and a single bad item failed the whole batch with that item's error. The workaround was to make sure no callback ever throws, returning p/rejected instead.

Suggested fix

Keep the per-task state local to the iteration, leaving p[STATE] read-only:

while (p[QUEUE].length) {
  const task = p[QUEUE].shift();
  let state = p[STATE];
  let rvalue;
  ...
}
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

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di funcool/promesa

Tutte le issue di funcool/promesa

Issue simili

Altre issue su Clojure

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.