Repository navigation
A fall-through error handler on an activity inside an IF branch is wired to the wrong place: an endless loop with a result variable, a silent end without one #1565
Description
Repro (v0.26.0, current main, and #1564; headerless and with mdl 1;; Mendix 11.12.5)
create or modify microflow Shop.ZZ_Callee () returns Integer begin return 1; end;
create or modify microflow Shop.ZZ_Callee0 () begin log info node 'Probe' 'callee'; end;
-- A: the call assigns a result
create or modify microflow Shop.ZZ_E1 ($Flag: Boolean)
begin
log info node 'Probe' 'before';
if $Flag then
$N = call microflow Shop.ZZ_Callee () on error without rollback begin
log warning node 'Probe' 'failed';
end error;
log info node 'Probe' 'after call';
end if;
log info node 'Probe' 'end';
end;
-- B: the same without a result variable
create or modify microflow Shop.ZZ_E2 ($Flag: Boolean)
begin
log info node 'Probe' 'before';
if $Flag then
call microflow Shop.ZZ_Callee0 () on error without rollback begin
log warning node 'Probe' 'failed';
end error;
log info node 'Probe' 'after call';
end if;
log info node 'Probe' 'end';
end;check and exec pass, and mx check reports 0 errors for both. The documented rule says a handler that doesn't end in RETURN or RAISE ERROR continues with the statement after the failing activity, which here is 'after call'. What gets stored instead:
- A, an endless loop. The handler's last activity flows into a new merge that sits in front of the
$Flagsplit. A failed call re-enters the decision, takes the same branch and calls again, for as long as the call keeps failing.describeshowsmerge rejoin1;before theifandjoin rejoin1;in the handler. - B, a silent end. The handler's last activity flows into a new EndEvent.
'after call'and'end'never run after a failure.describeprints areturn;inside the handler that the source never wrote.
The same thing happens:
- in either branch of an IF/ELSE;
- for
$Obj = create …with a handler; - for a handler on the last statement of a branch.
An empty handler, on error continue, a handler ending in RETURN, and a fall-through handler at the top level of the flow are all wired correctly.
A third shape, for you to rule on. In an IF/ELSE where the THEN branch ends in RETURN and the handler is in THEN while ELSE falls through, the handler rejoins at the IF's exit merge. On failure it runs the code after the IF and skips the rest of the THEN branch, including its return. That is deliberate code (thenNeedsErrorMerge), but it also contradicts the documented rule.
Cause
errorHandlerSource, errorHandlerTailFrom and errorHandlerSkipVar (cmd_microflows_builder.go) form one builder-wide pending-handler slot. At the top level the slot is settled when the next flow leaves the failing activity: buildFlowGraph calls addPendingErrorHandlerFlowForStatement.
In addIfStatement (cmd_microflows_builder_control.go), the branch loops only settle the empty-handler state: if fb.emptyErrorHandlerFrom == lastThenID, near lines 194 and 299, and the same in the enum-split loop. The IF-without-ELSE THEN loop has no such call at all. A non-empty handler's pending state therefore leaks out of the IF, and the outer builder consumes it:
- With a result variable (A): the outer loop draws
log → splitand callsaddPendingErrorHandlerFlowForStatement(log, split, …). BecauseskipVaris set,addPendingErrorHandlerFlowForState(cmd_microflows_builder_flows.go, around 342–355) splices a merge into that unrelated flow. Thestate.source == originIDguard (around line 358) is only on the non-skipVarpath. - Without one (B): nothing matches, so
terminatePendingErrorHandlersAtEnd(cmd_microflows_builder_graph.go, around 564) gives the handler a fresh EndEvent.
Suggested fix and a stopgap
Fix. Settle a non-empty handler's pending state inside each branch loop, keyed on errorHandlerSource == lastID and not only on emptyErrorHandlerFrom. Close it at the branch's exit, and add the source == originID guard to the skipVar path. A round-trip test is needed for both variants and for nested IFs.
Stopgap. Until then, check could refuse a non-empty fall-through handler on an activity inside an IF/ELSE or enum-split branch. bodyHasContinuingCustomErrorHandler and isContinuingCustomErrorHandler already detect the shape. A silent endless loop that mx check accepts is worse than a refusal.
Workaround: inside a branch, end the handler with RETURN and copy the remaining branch steps into it, or move the call into a sub-microflow called at the top level.
I can open a PR for this if you'd like. It's in the area you're working in this week (#1559), so tell me if you'd rather take it.
Activity
github-actions commentedon Oct 17, 2026
Issue Triage
Summary:
The issue describes a bug where a fall‑through error handler inside an IF/ELSE branch is incorrectly wired, causing either an endless loop (when the microflow assigns a result) or a silent early end (when it does not), contrary to the documented behavior that the handler should continue with the statement after the failing activity.
Checklist for bug report completeness
v0.26.0(also mentions currentmainand PR #1564)11.12.5ZZ_E1andZZ_E2) and ranmx check/mx exec'after call'and'end'log statements$Flagsplit)- Without result variable: silent end (handler jumps to an EndEvent, skipping
'after call'and'end')Missing information
To help us reproduce and diagnose the issue, could you please provide the output of
mxcli diag --bundle(or at least the relevant session log) for the commands you ran? This will give us the internal trace needed to verify the handler wiring.Thank you for the detailed report—once we have the diagnostic bundle we can move forward with investigating the fix.
Automated triage via OpenRouter — workflow source
ynnckw commentedon Oct 19, 2026
Reproduced on v0.27.0, Mendix 11.12.4; v0.26.0 stores the same flows. I read the stored sequence flows of each microflow. Both shapes from the report show up, in the ELSE branch too. Three more cases may help with the fix and its round-trip tests.
1. A REST call behaves the same way, and the result variable decides the shape. With
$Response = call rest service … returns response, a fall-through handler inside an IF leads into a new merge in front of the$Flagsplit. Withreturns nothing, it leads to a new empty end event instead. This matches$N = call microflow …andcall microflow …in the report.2. In a flow with a return value, the shape without a result depends on whether the return value is named.
returns Boolean as $Result, the handler's new end event returns$Result, a variable that does not exist, andmx checkreports CE0109. Of the shapes here, this is the only one any check reports.returns Booleanand no name, the handler leads into a new merge in front of the flow's finalreturn true;. After a failure the flow skips the rest of the branch and everything after the IF, and still returnstrue.mx checkreports nothing.3. An explicit
jointo a merge right after the failing activity wires the handler as the documented rule describes. This is a workaround until the fix lands, and it may also be the shape the builder could generate.Steps to reproduce
check -p … --referencesprints "Check passed!", andexeccreates all six microflows without a warning.Expected
In 1 to 4 the handler continues with
log 'after call', as it does in 5. This is the rule inmxcli syntax microflow error-handling: "A handler that does NOT end in RETURN/RAISE ERROR merges back into the main flow".Actual, read from the stored sequence flows; none of the microflows was run:
log 'failed'leads tomx check -wRestInIflog 'before'and the$Flagsplit, so a lasting failure calls again and againRestNothingInIfCallInIfNamed$Result[error] [CE0109] "Undefined variable 'Result'." at End eventCallInIfUnnamedreturn true)RestInIfJoinafterCall, thenlog 'after call'On the project after
exec,mx check -wreports that one CE0109 and no other new error or warning.describe microflowprints what was stored (@positionlines removed):A handler that ends in
return;(void flow) orreturn false;(flow with a return value) is stored correctly in the same positions, REST calls included.Possibly related: #1558, an
ifwithoutelseinside anon errorhandler, whose false flow is stored withNoCase. The nesting there is the other way round (an IF inside a handler, here a handler inside an IF), and the symptom differs. Both come from how the IF builder treats custom error handlers, so one round-trip test could cover both.Environment: mxcli v0.27.0 (2026-10-15) and v0.26.0, Mendix 11.12.4, Windows.