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
try_catch with no catch region discards its try-region step record on the returned-failure path (the other half of the engine's childSteps asymmetry) #14184
Found while implementing #13803 (a dying loop discarding its body's completed steps). Filed rather than fixed there: #13803 is scoped to the loop's throw path, and this is a different one. Not assigned — for triage to grade.
The gap
try_catch with no catch region does not contain the failure (measured and documented in #13681's reading: it is "identical to the control"). It returns a failing result, and on that return it deliberately withholds its childSteps:
packages/services/service-automation/src/builtin/try-catch-node.ts, the final return:
// No catch handler — surface the failure to the flow's fault edge / error
// handling. No `childSteps` here on purpose: the engine splices them only
// on a SUCCESSFUL node result, so attaching them to a failing one would
// be dead weight. That path is not the gap #7546 closes either — an
// unhandled failure already terminates the run `failed` with both
// run-level and step-level errors, which is loud by construction.
return { success: false, error: `try_catch '${node.id}': try region failed — ${lastError}` };
The reasoning was correct when it was written: the engine spliced childSteps only after a successful node result, so attaching them to a failing return really was dead weight. That is the same engine asymmetry #13803 is about.
Why it is worth a look now
#13803's fix closes the asymmetry for the throw path only — a dying container carries its completed body steps out on the thrown error and the engine's catch arm folds them in. It deliberately does not touch the engine's if (!result.success) branch, because no executor returns childSteps on a failing result today and adding a fold for zero producers is speculative.
So the returned-failure path is still unfolded, and try_catch with no handler still discards its try-region record. The observable shape is #13803's, one construct over: the try region's nodes may have written rows before one of them failed, and the run log keeps no step for any of them, so the #4354 summary under-reports acted in the same dangerous direction (an operator reads "nothing happened, safe to re-run" over writes that already landed).
Not measured here — I did not reproduce it, and the claim above is read off the source plus #13681's control reading, not off a run. Worth confirming before acting.
If it is graded as real
Two candidate shapes, both small, and the choice is a judgement call rather than a mechanical one:
Found while implementing #13803 (a dying
loopdiscarding its body's completed steps). Filed rather than fixed there: #13803 is scoped to the loop's throw path, and this is a different one. Not assigned — for triage to grade.The gap
try_catchwith nocatchregion does not contain the failure (measured and documented in #13681's reading: it is "identical to the control"). It returns a failing result, and on that return it deliberately withholds itschildSteps:packages/services/service-automation/src/builtin/try-catch-node.ts, the final return:The reasoning was correct when it was written: the engine spliced
childStepsonly after a successful node result, so attaching them to a failing return really was dead weight. That is the same engine asymmetry #13803 is about.Why it is worth a look now
#13803's fix closes the asymmetry for the throw path only — a dying container carries its completed body steps out on the thrown error and the engine's catch arm folds them in. It deliberately does not touch the engine's
if (!result.success)branch, because no executor returnschildStepson a failing result today and adding a fold for zero producers is speculative.So the returned-failure path is still unfolded, and
try_catchwith no handler still discards its try-region record. The observable shape is #13803's, one construct over: the try region's nodes may have written rows before one of them failed, and the run log keeps no step for any of them, so the #4354 summary under-reportsactedin the same dangerous direction (an operator reads "nothing happened, safe to re-run" over writes that already landed).Not measured here — I did not reproduce it, and the claim above is read off the source plus #13681's control reading, not off a run. Worth confirming before acting.
If it is graded as real
Two candidate shapes, both small, and the choice is a judgement call rather than a mechanical one:
childStepsto that failing return and foldresult.childStepsin the engine'sif (!result.success)branch — the symmetric half of [finding] A dyingloopdiscards its body's completed steps wholesale — the run summary reportsacted: 0while 5 writes actually happened (childSteps splice only after a SUCCESSFUL node result) #13803's change. Note this is the change [finding] A dyingloopdiscards its body's completed steps wholesale — the run summary reportsacted: 0while 5 writes actually happened (childSteps splice only after a SUCCESSFUL node result) #13803 measured in isolation and found to be a no-op for the loop; it would not be a no-op here, becausetry_catchgenuinely returns rather than throws.try_catchwith nocatch, which is Aloopnode aborts the entire flow run when one iteration's node fails — a single bad row kills a whole scheduled sweep, and there is no per-iteration containment to opt into #13681's ruled B-branch and may already cover it.They are not exclusive, and (2) may make (1) unnecessary if the spelling stops occurring.
Refs: #13803 (the throw path, fixed) · #13681 (the measurement that recorded the no-catch behaviour) · #7546 (the
partialStepssink).Generated by Claude Code