Bug: continue exits the loop when the same try/catch also contains a break (Lucee 6.2 and 7.0)

Inside a loop, if a try/catch contains both a continue and a break, executing the continue leaves the loop, as a break would.

  • The break doesn’t need to run: its presence anywhere in that try/catch is enough.
  • Positions measured: continue in the try with break in the catch, and both in the catch.
  • Without the break, continue behaves correctly.

There’s no error, so this is silent wrong control flow. We found it as a lost iteration in a batch loop: after one item hit the skip path, the remaining items weren’t processed.

Repro

Standalone, no framework needed. Expected output on every line: a,b,c.

<cfscript>
// Standalone repro: no framework needed. Expected output on every line: a,b,c
function tryContinueCatchBreak() {
	var trace = "";
	for (var item in ["a", "b", "c"]) {
		trace = ListAppend(trace, item);
		try {
			if (true) {
				continue;
			}
		} catch (any e) {
			break;
		}
	}
	return trace;
}
function indexLoop() {
	var trace = "";
	var items = ["a", "b", "c"];
	for (var i = 1; i <= 3; i++) {
		trace = ListAppend(trace, items[i]);
		try {
			if (true) {
				continue;
			}
		} catch (any e) {
			break;
		}
	}
	return trace;
}
function bothInCatch() {
	var trace = "";
	for (var item in ["a", "b", "c"]) {
		trace = ListAppend(trace, item);
		try {
			throw(type = "probe", message = "x");
		} catch (any e) {
			if (true) {
				continue;
			}
			break;
		}
	}
	return trace;
}
function controlNoBreak() {
	var trace = "";
	for (var item in ["a", "b", "c"]) {
		trace = ListAppend(trace, item);
		try {
			if (true) {
				continue;
			}
		} catch (any e) {
			var x = 1;
		}
	}
	return trace;
}
writeOutput("engine: " & server.coldfusion.productname & " " & (server.lucee.version ?: server.coldfusion.productversion) & chr(10));
writeOutput("1 for-in, continue in try, break in catch : " & tryContinueCatchBreak() & chr(10));
writeOutput("2 index loop, same                       : " & indexLoop() & chr(10));
writeOutput("3 continue and break both in catch       : " & bothInCatch() & chr(10));
writeOutput("4 control: no break in the try/catch     : " & controlNoBreak() & chr(10));
</cfscript>

Output on Lucee 7.0.0.395:

1 for-in, continue in try, break in catch : a
2 index loop, same                       : a
3 continue and break both in catch       : a
4 control: no break in the try/catch     : a,b,c

Measured builds

Engine Result
Lucee 6.2.5.48 wrong (continue exits the loop)
Lucee 7.0.1.100 wrong
Lucee 7.0.0.395 wrong
Adobe ColdFusion 2023.0.11 / 2025.0.06 correct (a,b,c)
BoxLang 1.11.0+52 correct

How each was measured:

  • 6.2.5.48 and 7.0.1.100: an equivalent TestBox probe (same shapes) in CI containers.
  • 7.0.0.395: that probe and the standalone repro above.

Also correct on every engine measured:

  • a loop nested inside a try that uses continue and break for that loop;
  • the same try/catch without the break.

The failing shape reproduces with for-in and index loops, with or without an outer try/finally.

Workaround

Avoid continue in such a try/catch; carry control to the end of the iteration with if/else.

I don’t have a Lucee Jira account. Could a Lucee maintainer open an LDEV issue for this? Thanks!

@bpamiri thanks a lot for the report, and for narrowing it down so well. The “the break only has to be there” detail took us straight to the cause.

We could reproduce it with your script: lines 1–3 return a, line 4 returns a,b,c on 6.2.9.4-RC, 7.0.6.9-RC, 7.1.1.15-RC and the current 8.0 snapshot. It isn’t a regression. 5.3, 5.4, 6.0 and 6.2.8 behave the same.

The cause is in the compiler. A break/continue inside a try goes through the try’s finally handling (a script try/catch always has some, even without finally). That handling had only one place for “where to go next”, so whichever break/continue appears last in that try decided it for all of them. That also means the reverse case is affected (if (x) break; continue; keeps looping), and so are cftry with cffinally and cfsilent.

Ticket: LDEV-6510
Pull request: LDEV-6510 continue must not act like break when the same try also contains a break by GianTschingt · Pull Request #2855 · lucee/Lucee · GitHub

The fix is a pull request against 7.1 (it merges up to 8.0), with a regression test that covers your cases plus a few more (while/do-while, finally, nested loops, labeled loops, tags).

Since this isn’t a regression, there’s no backport to 6.2 or 7.0 planned for now. On those versions, keep continue and break out of the same try/catch. Set a flag inside it and act on the flag after the try:

for (var item in items) {
	var action = "";
	try {
		if (skip(item)) action = "continue";
		else process(item);
	} catch (any e) {
		action = "break";
	}
	if (action == "continue") continue;
	if (action == "break") break;
}

A labeled continue/break does not help, by the way: it goes through the same code and shows the same problem.