Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

select/race flattens activity errors into the winner's value instead of surfacing a failure

Aperta
#9 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Tranquilla
Stack tecnologico
javascript, rust

Direzione di ricerca

Inizia da src/handlers.rs, in particolare da make_select_future e ScheduledTask::Select, quindi confronta il percorso di errore diretto in handlers.rs e lib/duroxide.js con make_join_future. Esegui la riproduzione RaceBoom fornita e ispeziona i punti di ingresso race() / raceTyped(). Il lavoro è completato quando un ramo vincente che ha avuto esito negativo non può essere scambiato per un valore riuscito e il comportamento esistente di select è coperto dalla validazione di regressione.

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

Descrizione

Summary

When an activity participates in ctx.race() / ctx.raceTyped() and fails, the select resolves successfully with the branch's raw error string as its value instead of surfacing a failure. The orchestration receives { index: 0, value: "<error message>" } and has no way to distinguish a failed activity from a successful activity that legitimately returned that same string.

This is inconsistent with the SDK's own semantics elsewhere:

  • A directly yielded activity that fails is delivered via driveStepWithError → gen.throw(...) — the orchestration sees a thrown error (handlers.rs builds the step payload with isError, lib/duroxide.js calls gen.throw when isError is set).
  • ctx.all() / ctx.allTyped() (join) deliberately preserves the distinction: make_join_future wraps each branch as {ok: v} / {err: e} ("Unlike make_select_future, this preserves the ok/err distinction so JS can tell success from failure").

Only select flattens. The comment in src/handlers.rs documents it as intentional:

/// Convert a ScheduledTask into a type-erased future returning a raw string for use in select.
/// Activity/sub-orch errors are flattened (Ok and Err both become the raw string value).
fn make_select_future(...) -> ... {
    match task {
        ScheduledTask::Activity { .. } => Box::pin(async move {
            ...
            match future.await {
                Ok(v) => v,
                Err(e) => e,   // <-- error becomes the winning *value*
            }
        }),
        ...

The same flattening applies to ActivityWithRetry, SubOrchestration*, and GetValueFromInstance branches.

Why this is a footgun

The failure is silent. Orchestration code like:

const winner = yield ctx.race(
    ctx.scheduleActivity("CallModel", input),   // may fail
    ctx.dequeueEvent("cancel"),
);
if (winner.index === 0) {
    const result = JSON.parse(winner.value);    // error string flows in here
    ...
}

happily treats the error message as the activity's result. Any error-handling try/catch around the yield — which works correctly for the direct-yield form — never fires. If the activity's legitimate return type is a plain string, the two cases are indistinguishable even in principle.

We hit this in PilotSwarm while converting a directly-yielded long-running activity into race(activityTask, dequeueEvent(stopQueue)) for a stop-button feature: the conversion silently disabled the entire activity retry path until we noticed the flattening in the bridge source and added a shape-sniffing workaround (parse the value; if it isn't the expected JSON payload, re-throw it as an error). That workaround only works because our activity returns structured JSON, not strings.

Repro

runtime.registerActivity("Boom", async () => { throw new Error("kaboom"); });

runtime.registerOrchestration("RaceBoom", function* (ctx) {
    try {
        const winner = yield ctx.race(
            ctx.scheduleActivity("Boom", null),
            ctx.scheduleTimer(60_000),
        );
        // Reached with winner = { index: 0, value: "kaboom" } — no throw.
        return `unexpected success: ${JSON.stringify(winner)}`;
    } catch (err) {
        return `caught: ${err.message}`;   // never reached
    }
});

Expected (to match direct-yield semantics): the catch fires, or the winner carries an explicit error marker.
Actual: the orchestration completes with unexpected success: {"index":0,"value":"kaboom"}.

Possible fixes

  1. Throw into the generator when the select winner is a failed branch — consistent with the direct-yield contract. (Losing branches are already cancel-requested; only the winner's disposition changes.)
  2. Preserve the marker like join does: resolve select with { index, ok } / { index, err } (or { index, value, isError }). Breaking change for existing callers, but the current shape is unreliable anyway.
  3. At minimum, document the flattening prominently on race() / raceTyped() in the README and JSDoc — today it's only visible in a Rust source comment.

Option 1 seems most consistent; option 2 is more expressive if you'd rather races never throw.

Environment

  • duroxide-node 0.1.27 (npm), also verified against current main sources (src/handlers.rs make_select_future / ScheduledTask::Select handling)
  • macOS arm64, Node 20+
Lingua principale
JavaScript
Stelle
36
Fork
20
Merge medio
2g 9h
PR unite (30g)
3

Preparare l'ambiente

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 microsoft/duroxide-node

Tutte le issue di microsoft/duroxide-node

Issue simili

Altre issue su JavaScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.