> The projects examined contained a total of 120,964,221 lines of Python code, and among them the script found 203 instances of control flow instructions in a finally block. Most were return, a handful were break, and none were continue.
I don't really write a lot of Python, but I do write a lot of Java, and `continue` is the main control flow statement that makes sense to me within a finally block.
I think it makes sense when implementing a generic transaction loop, something along the lines of:
<T> T executeTransaction(Function<Transaction, T> fn) {
for (int tries = 0;; tries++) {
var tx = newTransaction();
try {
return fn.apply(tx);
} finally {
if (!tx.commit()) {
// TODO: potentially log number of tries, maybe include a backoff, maybe fail after a certain number
continue;
}
}
}
}
In these cases "swallowing" the exception is often intentional, since the exception could be due to some logic failing as a result of inconsistent reads, so the transaction should be retried.
The alternative ways of writing this seem more awkward to me. Either you need to store the result (returned value or thrown exception) in one or two variables, or you need to duplicate the condition and the `continue;` behaviour. Having the retry logic within the `finally` block seems like the best way of denoting the intention to me, since the intention is to swallow the result, whether that was a return or a throw.
If there are particular exceptions that should not be retried, these would need to be caught/rethrown and a boolean set to disable the condition in the `finally` block, though to me this still seems easier to reason about than the alternatives.
> Having the retry logic within the `finally` block seems like the best way of denoting the intention to me, since the intention is to swallow the result, whether that was a return or a throw.
Except that is not the documented intent of the `finally` construct:
The finally block always executes when the try block exits.
This ensures that the finally block is executed even if an
unexpected exception occurs. But finally is useful for more
than just exception handling — it allows the programmer to
avoid having cleanup code accidentally bypassed by a
return, continue, or break. Putting cleanup code in a
finally block is always a good practice, even when no
exceptions are anticipated.[0]
Using `finally` for implementing retry logic can be done, as you have illustrated, but that does not mean it is "the best way of denoting the intention." One could argue this is a construct specific to Java (the language) and does not make sense outside of this particular language-specific idiom.
> The intent of the transaction code is that the consistency is checked (using `tx.commit()`) "even if an unexpected exception occurs".
First, having a `commit` unconditionally attempted when an exception is raised would surprise many developers. Exceptions in transactional logic are often used to represent a "rollback persistent store changes made thus far" scenario.
Second, using a condition within `finally` to indicate a retry due to a `commit` failing could be expressed in a clearer manner by having it within the `try` block as described by IntelliJ here[0].
> Exceptions in transactional logic are often used to represent a "rollback persistent store changes made thus far" scenario.
Handling can be added to change the transaction to be read-only if the inner code throws a particular exception, but the consistency should still be checked through a `commit` phase (at least in an OCC setting), so the `continue` in `finally` is still the correct way to do it.
> could be expressed in a clearer manner by having it within the `try` block as described by IntelliJ here[0].
Wrong link? The only solution I see there is to add a comment to suppress the warning, which sounds fine to me (eg, analogous to having a `// fallthrough` comment when intentionally omitting `break` statements within `switch`, since I can agree that both of these things are uncommon, but sometimes desirable).
> Handling can be added to change the transaction to be read-only if the inner code throws a particular exception, but the consistency should still be checked through a `commit` phase (at least in an OCC setting), so the `continue` in `finally` is still the correct way to do it.
This approach fails to account for `fn` performing multiple mutations where an exception is raised from statement N, where N > 1.
For example, suppose `fn` successfully updates a record in table `A`, then attempts to insert a record into table `B` which produces a constraint violation exception[0]. Unconditionally performing a `commit` in the `finally` block will result in the mutation in table `A` being persisted, thus resulting in an invalid system persistent state.
If the `try` block performed the `commit` and the `finally` block unconditionally performed a `rollback`, then the behavior I believe sought would be sound.
No, it's the link I intended. The purpose of it was to provide the warning anyone working in a translation unit using the technique originally proffered would see as well as be a starting point for research.
> The intent of the transaction code is that the consistency is checked (using `tx.commit()`) "even if an unexpected exception occurs".
A transaction failing is the opposite of an unexpected event. Transactions failing is a central use case of any transaction. Therefore it should be handled explicitly instead of using exceptions.
Exceptions are for unexpected events such as the node running out of memory, or a process failing to write to disk.
> A transaction failing is the opposite of an unexpected event.
That's why it's denoted by a non-exceptional return value from `tx.commit()` in my sample code. When I've talked about exceptions here, I'm talking about exceptions raised within the transaction. If the transaction succeeds, those exceptions should be propagated to the calling code.
> Exceptions are for unexpected events such as the node running out of memory, or a process failing to write to disk.
Discussing valid uses of exceptions seems orthogonal to this (should OOM lead to a catchable exception [0], or should it crash the process?). In any case, if the process is still alive and the transaction code determines without error that "yes, this transaction was invalid due to other contending transactions", it should retry the transaction. If something threw due to lack of memory or disk space, chances are it will throw again within a successful transaction and the error will be propagated.
[0] As alluded to in my first post, you might want to add some special cases for exceptions/errors that you want to immediately propagate instead of retrying. Eg, you might treat `Error` subtypes differently, which includes `OutOfMemoryError` and other cases that suggest the program is in a potentially unusable state, but this still isn't required according to the intent of the transactional logic.
Doesn't that code ignore errors even if it runs out of retries? Don't you want to log every Exception that happens, even if the transaction will be retried?
A result of an inconsistent transaction should be discarded whether it's a return value or a thrown exception. If it runs out of tries another error should be thrown. This should only happen due to contention (overlapping transactions), not due to a logical exception within the transaction.
You can add extra logging to show results or exceptions within the transaction if you want (for the exception this would simply be a `catch` just before the `finally` that logs and rethrows).
I've omitted these extra things because it's orthogonal to the point that the simplest way to express this logic is by having the `continue` control flow unconditional on whether the code was successful .. which is what you use `finally` for.
If you did this in Rust noone would complain, since the overall result is expressed as a first-class `Result<T, E>` value that can naturally be discarded. This is why Rust doesn't have `finally`.
Rust is also a lot more permissive about use of control flow, since you can write things like `foo(if x { y } else { continue }, bar)`.
Personally, I prefer when the language gives a bit more flexibility here. Of course you can write things that are difficult to understand, but my stance is still that my example code above is the simplest way to write the intended logic, until someone demonstrates otherwise.
I don't think this is a restriction that generally helps with code quality. If anything I've probably seen more bad code due to a lack of finding the simplest way to express control flow of an algorithm.
I'm sure there's some train of thought that says that continue/break/return from a loop is bad (see proponents of `Array.prototype.forEach` in JS), but I disagree with it.
and the principle still applies. The simplest solution still involves a `continue` within the `finally` block.
Whether it's a good idea to actually do this directly using SQL connections is another question .. SQL databases usually use pessimistic locking, where the transaction failures are actually "deadlocks" that are preferably avoided through careful ordering of operations within the transaction (or more commonly, YOLOing it using an isolation level that allows read anomalies). Without going into all the details, this probably has a large influence over the design of the SQL APIs you're referring to.
You want to at least check that the exception was raised in the absence of read anomalies. The check for read anomalies in OCC happens during the commit phase.
Setting a transaction to read-only on error is possible using the code (using a rethrowing catch within the transaction), but this is not universally desirable.
If you're using transactions to run fairly arbitrary code atomically (assuming no static state outside of the transaction), the expected behaviour would be that modifications leading up to an exception (in a non-anomalous transaction) are still persisted. Eg, imagine the code within the transaction is updating a persisted retry counter before performing a fallible operation. In this case you want the counter to be updated so that the transaction doesn't just fail an infinite number of times, since each time you roll back on error you're just restoring the state that leads to the error.
Another case would be where the exception is due to something that was written within the transaction. If the exception were raised but the writes were not persisted, it would at least be confusing seeing the exception, and possibly logically incorrect depending on how the irrelevant exception is handled (since it's due to data that theoretically doesn't exist).
I would not call this snippet particularly "cursed". There is no "aktshchually this happens, therefore this is activated" hidden magic going on. The try-catch-finally construct is doing exactly what it is designed and documented to do: finally block is executed regardless of the outcome within try. The purpose of finally block is to fire regardless of exceptionality in control flow.
Surprising at first? Maybe. Cursed? Wouldn't say so. It is merely unconventional use of the construct.
It messes with the semantics of "return" statement. The conventional intuition is that right after a "return" statement completes, the current method's invocation ends and the control returns to the caller. Unfortunately, the actual semantics has to be that is "attempts to return control":
The preceding descriptions say "attempts to transfer control" rather than just "transfers control" because if there are any try statements (§14.20) within the method or constructor whose try blocks or catch clauses contain the return statement, then any finally clauses of those try statements will be executed, in order, innermost to outermost, before control is transferred to the invoker of the method or constructor. Abrupt completion of a finally clause can disrupt the transfer of control initiated by a return statement.
This, I believe, is the only way for "return E", after the evaluation of E completes normally, to not return the E's value to the caller. Thanks for a needless corner-case complication, I guess.
> It messes with the semantics of "return" statement.
Yes, that's what exceptions and their handling handling does - it messes with control flow.
> The conventional intuition is that right after a "return" statement completes, the current method's invocation ends and the control returns to the caller.
It is part of similar conventional thinking that "a method call must return back to the caller on termination", which is not even applicable to Java and other languages with conventional exceptions.
> Thanks for a needless corner-case complication, I guess.
But what is the alternative otherwise? Not execute finally block if try block exits normally? Execute finally block, but ignore control flow statements? What if code in finally block throws? I maintain that execution of finally block, including control flow statements within, normally is the only sane behavior.
> It is part of similar conventional thinking that "a method call must return back to the caller on termination".
No, it's a part of a conventional thinking that "if a return statement executes without any exceptions thrown in the process, then the control will return from the current invocation". It's an extension of a conventional thinking "if an e.g. assignment statement executes without any exceptions thrown in the process, the control will go to the next statement". Really helps with reasoning about the program behaviour without spending an undue amount of brainpower.
> But what is the alternative otherwise?
Disallow "return", "break", "continue", and "goto" statements inside "finally" blocks, like C# does. Nobody sane misses this functionality anyway.
Yes, very much is... return is not a single statement or a single opcode. the call to f() is returned on the stack, that's assigned to some local variable via istore, there is no 'ireturn', yet. Then the g() method is called along with whatever is printed, and the next instruction can be ireturn. which would return '2' indeed.
It is a single statement. It's just that executing it contains of two parts: evaluating the expression, and the non-local transfer of control ― and the weird part is that this control transfer can be hijacked. And it is weird, you can't do this with e.g. "if": if the test condition is evaluated successfully, then the control will go into one of the branches, and you can't mess with that... but with "return", you can abort this transfer of control. Which is, again, weird: all other expression-evaluating statements can go haywire only if the evaluation of the expression inside them throws an exception — but if that doesn't happen, the control transfer is completely trivial, with no special rules about try-catch blocks.
Mo, we were talking about what would be reasonable semantics for the interaction of two features in a high-level programming language Java; the semantics of bytecode doesn't really matter in this case. The precedent for that would be that "finally"m in fact, really doesn't mesh all that well with what is available for bytecode, so javac has to resort to code duplication.
Sufficiently unexpected (surprising) is cursed. If anything, that’s the way most people use that term for programming language constructs (weird coercions in JavaScript, conflicting naming conventions in PHP, overflow checks that get erased by the compiler due to UB in c/c++, etc.)
I think there three distinct classes of surprising that fall under this umbrella: 1. a behavior overrides another behavior due to some precedence rules 2. behavior of a construct in language x is different than behavior of similar construct in most mainstream languages 3. who in their right mind would design the thing to behave like this.
Examples could be: string "arithmetic" in most dynamically typed languages, PHP's ternary operator order of precedence and Python's handling of default function arguments. In all of these cases when you sit down, think about it and ask yourself "what's the alternative?", there's always a very reasonable answer to it, therefore, I think it is reasonable to call these behaviors cursed.
In this case the surprise factor comes in, because we are used to equating finally block with cleanup and I concur that many would trip on this the first time. But if you gave this exercise some time and asked "what should happen if finally block contains control flow statements?" the reasonable answer should be "take precedence", because the behavior would be surprising/cursed otherwise.
> In this case the surprise factor comes in, because we are used to equating finally block with cleanup and I concur that many would trip on this the first time. But if you gave this exercise some time and asked "what should happen if finally block contains control flow statements?" the reasonable answer should be "take precedence", because the behavior would be surprising/cursed otherwise.
Wouldn't the reasonable behaviour be "throw a compiler error"?
> But if you gave this exercise some time and asked "what should happen...
This applies to any cursed JavaScript code too (see https://jswtf.com). The increased cognitive load required to figure out unintuitive/counterintuitive code is what makes it cursed.
It's been many, many moons since I touched Java but I would have expected this to run the finally { } clause and then return from the function (similar to how in C++, objects on the stack will run their destructors after a return call before the function ends. I certainly wouldn't expect it to cancel the return.
Yes that is the one exception (heh) to the rule unfortunately. You can throw from anywhere so it must support unwinding from any point. So if you were really intent on abusing the finally you could wrap the try-finally in a try-catch and then throw from the finally and put your continue in the catch.
The finally behave slightly different in CIL. You have protected regions and finally/fault/catch/filters handlers attached. So in order to support continue inside finally you should introduce some state machine , which is complication and generally against Roslyn design limitation.
That's not just Java and there is nothing really cursed about it: throwing in a finally block is the most common example. Jump statements are no different, you can't just ignore them when they override the return or throw statements.
Notably, C++ and similar languages don't support lexical `finally` at all, instead relying on destructors, which are a function and obviously cannot affect the control flow of their caller ...
except by throwing exceptions, which is a different problem that there's no "good" solution to (during unwinding, that is).
I thought destructors were all noexcept now... or at the very least if you didn't noexcept, and then threw something, it just killed the process.
Although, strictly speaking, they could have each exception also hold a reference to the prior exception that caused the excepting object to be destroyed. This forms an intrusive linked list of exceptions. Problem is, in C++ you can throw any value, so there isn't exactly any standard way for you to get the precursor exception, or any standard way for the language to tell the exception what its precursor was. In Python they could just add a field to the BaseException class that all throwables have to inherit from.
Destructors are noexcept by default, but that can be overridden with noexcept(false).
> or at the very least if you didn't noexcept, and then threw something, it just killed the process.
IIRC throwing out of a destructor that's marked noexcept(false) terminates the process only if you're already unwinding from something else. Otherwise the exception should be thrown "normally".
try/finally is effectively try/catch(Throwable) with copy all the code of the finally block prior to exiting the method. (Java doesn't have a direct bytecode support for 'finally')
this broke my head. I think I haven't touched Java in a while and kept thinking continue should be in a case/switch so ittook a minute to back out of that alleyway before I even got what was wrong with this.
If by coroutines the author meant virtual threads, then monitors have always been compatible with virtual threads (which have always needed to adhere to the Thread specification). Monitors could, for a short while, degrade the scalability of virtual threads (and in some situations even lead to deadlocks), but that has since been resolved in JDK 24 (https://openjdk.org/jeps/491).
I think it's coroutines as in other JVM languages like Kotlin, where yielding may be implemented internally as return (due to lack of native coroutine support in JVM).
Holding a lock/monitor across a yield is a bad idea for other reasons, so it shouldn't be a big deal in practice.
Older versions of Java did try to have only one copy of the finally block code. To implement this, there were "jsr" and "ret" instructions, which allowed a method (a subroutine) to contain subroutines inside it. This even curseder implementation of finally is prohibited starting from version 51 class files (Java 7).
Doesn't JRE has some limited form of decompilation in its JIT, as a pre-pass? IIRC, it reconstructs the basic blocks and CFG from the bytecode and does some minor optimizations before going on to regalloc and codegen.
On the subject
is my favorite cursed Java exceptions construct.Python has the same construct but is removing it, starting with a warning in version 3.14: https://peps.python.org/pep-0765/
There was a recent talk at PYCON UK about it, by one of the authors of the PEP in question: <https://www.youtube.com/watch?v=vrVXgeD2fts>
Interesting .. from the post above:
> The projects examined contained a total of 120,964,221 lines of Python code, and among them the script found 203 instances of control flow instructions in a finally block. Most were return, a handful were break, and none were continue.
I don't really write a lot of Python, but I do write a lot of Java, and `continue` is the main control flow statement that makes sense to me within a finally block.
I think it makes sense when implementing a generic transaction loop, something along the lines of:
In these cases "swallowing" the exception is often intentional, since the exception could be due to some logic failing as a result of inconsistent reads, so the transaction should be retried.The alternative ways of writing this seem more awkward to me. Either you need to store the result (returned value or thrown exception) in one or two variables, or you need to duplicate the condition and the `continue;` behaviour. Having the retry logic within the `finally` block seems like the best way of denoting the intention to me, since the intention is to swallow the result, whether that was a return or a throw.
If there are particular exceptions that should not be retried, these would need to be caught/rethrown and a boolean set to disable the condition in the `finally` block, though to me this still seems easier to reason about than the alternatives.
> Having the retry logic within the `finally` block seems like the best way of denoting the intention to me, since the intention is to swallow the result, whether that was a return or a throw.
Except that is not the documented intent of the `finally` construct:
Using `finally` for implementing retry logic can be done, as you have illustrated, but that does not mean it is "the best way of denoting the intention." One could argue this is a construct specific to Java (the language) and does not make sense outside of this particular language-specific idiom.Conceptually, "retries" are not "cleanup code."
0 - https://docs.oracle.com/javase/tutorial/essential/exceptions...
Sounds like the right intent to me. To pinpoint your existing quote from the documentation:
> The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs.
The intent of the transaction code is that the consistency is checked (using `tx.commit()`) "even if an unexpected exception occurs".
I'm not sure how else to interpret that to be honest. If you've got a clearer way of expressing this, feel free to explain.
> The intent of the transaction code is that the consistency is checked (using `tx.commit()`) "even if an unexpected exception occurs".
First, having a `commit` unconditionally attempted when an exception is raised would surprise many developers. Exceptions in transactional logic are often used to represent a "rollback persistent store changes made thus far" scenario.
Second, using a condition within `finally` to indicate a retry due to a `commit` failing could be expressed in a clearer manner by having it within the `try` block as described by IntelliJ here[0].
0 - https://www.jetbrains.com/help/inspectopedia/ContinueOrBreak...
> Exceptions in transactional logic are often used to represent a "rollback persistent store changes made thus far" scenario.
Handling can be added to change the transaction to be read-only if the inner code throws a particular exception, but the consistency should still be checked through a `commit` phase (at least in an OCC setting), so the `continue` in `finally` is still the correct way to do it.
> could be expressed in a clearer manner by having it within the `try` block as described by IntelliJ here[0].
> 0 - https://www.jetbrains.com/help/inspectopedia/ContinueOrBreak...
Wrong link? The only solution I see there is to add a comment to suppress the warning, which sounds fine to me (eg, analogous to having a `// fallthrough` comment when intentionally omitting `break` statements within `switch`, since I can agree that both of these things are uncommon, but sometimes desirable).
> Handling can be added to change the transaction to be read-only if the inner code throws a particular exception, but the consistency should still be checked through a `commit` phase (at least in an OCC setting), so the `continue` in `finally` is still the correct way to do it.
This approach fails to account for `fn` performing multiple mutations where an exception is raised from statement N, where N > 1.
For example, suppose `fn` successfully updates a record in table `A`, then attempts to insert a record into table `B` which produces a constraint violation exception[0]. Unconditionally performing a `commit` in the `finally` block will result in the mutation in table `A` being persisted, thus resulting in an invalid system persistent state.
If the `try` block performed the `commit` and the `finally` block unconditionally performed a `rollback`, then the behavior I believe sought would be sound.
>> 0 - https://www.jetbrains.com/help/inspectopedia/ContinueOrBreak...
> Wrong link?
No, it's the link I intended. The purpose of it was to provide the warning anyone working in a translation unit using the technique originally proffered would see as well as be a starting point for research.
0 - https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/...
> The intent of the transaction code is that the consistency is checked (using `tx.commit()`) "even if an unexpected exception occurs".
A transaction failing is the opposite of an unexpected event. Transactions failing is a central use case of any transaction. Therefore it should be handled explicitly instead of using exceptions.
Exceptions are for unexpected events such as the node running out of memory, or a process failing to write to disk.
> A transaction failing is the opposite of an unexpected event.
That's why it's denoted by a non-exceptional return value from `tx.commit()` in my sample code. When I've talked about exceptions here, I'm talking about exceptions raised within the transaction. If the transaction succeeds, those exceptions should be propagated to the calling code.
> Exceptions are for unexpected events such as the node running out of memory, or a process failing to write to disk.
Discussing valid uses of exceptions seems orthogonal to this (should OOM lead to a catchable exception [0], or should it crash the process?). In any case, if the process is still alive and the transaction code determines without error that "yes, this transaction was invalid due to other contending transactions", it should retry the transaction. If something threw due to lack of memory or disk space, chances are it will throw again within a successful transaction and the error will be propagated.
[0] As alluded to in my first post, you might want to add some special cases for exceptions/errors that you want to immediately propagate instead of retrying. Eg, you might treat `Error` subtypes differently, which includes `OutOfMemoryError` and other cases that suggest the program is in a potentially unusable state, but this still isn't required according to the intent of the transactional logic.
Doesn't that code ignore errors even if it runs out of retries? Don't you want to log every Exception that happens, even if the transaction will be retried?
This code is totally rotten.
A result of an inconsistent transaction should be discarded whether it's a return value or a thrown exception. If it runs out of tries another error should be thrown. This should only happen due to contention (overlapping transactions), not due to a logical exception within the transaction.
You can add extra logging to show results or exceptions within the transaction if you want (for the exception this would simply be a `catch` just before the `finally` that logs and rethrows).
I've omitted these extra things because it's orthogonal to the point that the simplest way to express this logic is by having the `continue` control flow unconditional on whether the code was successful .. which is what you use `finally` for.
If you did this in Rust noone would complain, since the overall result is expressed as a first-class `Result<T, E>` value that can naturally be discarded. This is why Rust doesn't have `finally`.
Rust is also a lot more permissive about use of control flow, since you can write things like `foo(if x { y } else { continue }, bar)`.
Personally, I prefer when the language gives a bit more flexibility here. Of course you can write things that are difficult to understand, but my stance is still that my example code above is the simplest way to write the intended logic, until someone demonstrates otherwise.
I don't think this is a restriction that generally helps with code quality. If anything I've probably seen more bad code due to a lack of finding the simplest way to express control flow of an algorithm.
I'm sure there's some train of thought that says that continue/break/return from a loop is bad (see proponents of `Array.prototype.forEach` in JS), but I disagree with it.
> if (!tx.commit())
https://docs.oracle.com/javase/8/docs/api/java/sql/Connectio...:
⇒ this code I won’t even compile for the java.sql.Transaction” class that is part of the Java platform.(I think having commit throw on error is fairly common. Examples: C# (https://learn.microsoft.com/en-us/dotnet/api/system.data.sql...), Python (https://docs.python.org/3/library/sqlite3.html#sqlite3.Conne...))
I wasn't thinking of JDBC SQL transactions specifically, but sure, different APIs can denote retriable transaction failures differently. Instead of:
you do: and the principle still applies. The simplest solution still involves a `continue` within the `finally` block.Whether it's a good idea to actually do this directly using SQL connections is another question .. SQL databases usually use pessimistic locking, where the transaction failures are actually "deadlocks" that are preferably avoided through careful ordering of operations within the transaction (or more commonly, YOLOing it using an isolation level that allows read anomalies). Without going into all the details, this probably has a large influence over the design of the SQL APIs you're referring to.
This code is wrong. You don't want to commit a transaction if an exception is thrown during the transaction.
You want to at least check that the exception was raised in the absence of read anomalies. The check for read anomalies in OCC happens during the commit phase.
Setting a transaction to read-only on error is possible using the code (using a rethrowing catch within the transaction), but this is not universally desirable.
If you're using transactions to run fairly arbitrary code atomically (assuming no static state outside of the transaction), the expected behaviour would be that modifications leading up to an exception (in a non-anomalous transaction) are still persisted. Eg, imagine the code within the transaction is updating a persisted retry counter before performing a fallible operation. In this case you want the counter to be updated so that the transaction doesn't just fail an infinite number of times, since each time you roll back on error you're just restoring the state that leads to the error.
Another case would be where the exception is due to something that was written within the transaction. If the exception were raised but the writes were not persisted, it would at least be confusing seeing the exception, and possibly logically incorrect depending on how the irrelevant exception is handled (since it's due to data that theoretically doesn't exist).
To anyone wondering, I believe it's cursed because the finally continue blocks hijacks the try return, so the for loop never returns
So the function returns, and then during its tidyup, the 'continue' basically comefrom()s the VM back into the loop? That is, indeed, cursed.
I would not call this snippet particularly "cursed". There is no "aktshchually this happens, therefore this is activated" hidden magic going on. The try-catch-finally construct is doing exactly what it is designed and documented to do: finally block is executed regardless of the outcome within try. The purpose of finally block is to fire regardless of exceptionality in control flow.
Surprising at first? Maybe. Cursed? Wouldn't say so. It is merely unconventional use of the construct.
It messes with the semantics of "return" statement. The conventional intuition is that right after a "return" statement completes, the current method's invocation ends and the control returns to the caller. Unfortunately, the actual semantics has to be that is "attempts to return control":
This, I believe, is the only way for "return E", after the evaluation of E completes normally, to not return the E's value to the caller. Thanks for a needless corner-case complication, I guess.> It messes with the semantics of "return" statement.
Yes, that's what exceptions and their handling handling does - it messes with control flow.
> The conventional intuition is that right after a "return" statement completes, the current method's invocation ends and the control returns to the caller.
It is part of similar conventional thinking that "a method call must return back to the caller on termination", which is not even applicable to Java and other languages with conventional exceptions.
> Thanks for a needless corner-case complication, I guess.
But what is the alternative otherwise? Not execute finally block if try block exits normally? Execute finally block, but ignore control flow statements? What if code in finally block throws? I maintain that execution of finally block, including control flow statements within, normally is the only sane behavior.
> It is part of similar conventional thinking that "a method call must return back to the caller on termination".
No, it's a part of a conventional thinking that "if a return statement executes without any exceptions thrown in the process, then the control will return from the current invocation". It's an extension of a conventional thinking "if an e.g. assignment statement executes without any exceptions thrown in the process, the control will go to the next statement". Really helps with reasoning about the program behaviour without spending an undue amount of brainpower.
> But what is the alternative otherwise?
Disallow "return", "break", "continue", and "goto" statements inside "finally" blocks, like C# does. Nobody sane misses this functionality anyway.
finally stuff is executed prior to the 'return'; effectively it's placed before the 'return' statement. So it's quite obvious.
No, it's not executed before the return, it's executed in the middle of it, so to speak:
will print If "finally" block was executed before the "return" in the "try", the call to f() would not have been made.Yes, very much is... return is not a single statement or a single opcode. the call to f() is returned on the stack, that's assigned to some local variable via istore, there is no 'ireturn', yet. Then the g() method is called along with whatever is printed, and the next instruction can be ireturn. which would return '2' indeed.
> return is not a single statement.
It is a single statement. It's just that executing it contains of two parts: evaluating the expression, and the non-local transfer of control ― and the weird part is that this control transfer can be hijacked. And it is weird, you can't do this with e.g. "if": if the test condition is evaluated successfully, then the control will go into one of the branches, and you can't mess with that... but with "return", you can abort this transfer of control. Which is, again, weird: all other expression-evaluating statements can go haywire only if the evaluation of the expression inside them throws an exception — but if that doesn't happen, the control transfer is completely trivial, with no special rules about try-catch blocks.
you cut the statement in the middle, it's very obviously in terms of bytecode as we talk about bytecode generation.
Mo, we were talking about what would be reasonable semantics for the interaction of two features in a high-level programming language Java; the semantics of bytecode doesn't really matter in this case. The precedent for that would be that "finally"m in fact, really doesn't mesh all that well with what is available for bytecode, so javac has to resort to code duplication.
Sufficiently unexpected (surprising) is cursed. If anything, that’s the way most people use that term for programming language constructs (weird coercions in JavaScript, conflicting naming conventions in PHP, overflow checks that get erased by the compiler due to UB in c/c++, etc.)
> Sufficiently unexpected (surprising) is cursed.
I think there three distinct classes of surprising that fall under this umbrella: 1. a behavior overrides another behavior due to some precedence rules 2. behavior of a construct in language x is different than behavior of similar construct in most mainstream languages 3. who in their right mind would design the thing to behave like this.
Examples could be: string "arithmetic" in most dynamically typed languages, PHP's ternary operator order of precedence and Python's handling of default function arguments. In all of these cases when you sit down, think about it and ask yourself "what's the alternative?", there's always a very reasonable answer to it, therefore, I think it is reasonable to call these behaviors cursed.
In this case the surprise factor comes in, because we are used to equating finally block with cleanup and I concur that many would trip on this the first time. But if you gave this exercise some time and asked "what should happen if finally block contains control flow statements?" the reasonable answer should be "take precedence", because the behavior would be surprising/cursed otherwise.
That's my reasoning.
> In this case the surprise factor comes in, because we are used to equating finally block with cleanup and I concur that many would trip on this the first time. But if you gave this exercise some time and asked "what should happen if finally block contains control flow statements?" the reasonable answer should be "take precedence", because the behavior would be surprising/cursed otherwise.
Wouldn't the reasonable behaviour be "throw a compiler error"?
Can the compiler perform sufficiently deep static analysis to cover all possible cases? At least in Java it is by definition impossible.
I don't see why not. If it's in a finally block, disallow control flow. Or am I missing something?
If it's that hard to figure out what the code should do by looking at, then it's probably not a great thing to allow in the language.
> But if you gave this exercise some time and asked "what should happen...
This applies to any cursed JavaScript code too (see https://jswtf.com). The increased cognitive load required to figure out unintuitive/counterintuitive code is what makes it cursed.
It's been many, many moons since I touched Java but I would have expected this to run the finally { } clause and then return from the function (similar to how in C++, objects on the stack will run their destructors after a return call before the function ends. I certainly wouldn't expect it to cancel the return.
see, if you only had GOTO's, this would be obvious what is going on!
Just tested that in C# and it seems they made the smart decision to not allow shenanigans like that in a finally block:
CS0157 Control cannot leave the body of a finally clause
What about throwing an exception from the finally clause?
This loops, if that's what you're asking:
Yes that is the one exception (heh) to the rule unfortunately. You can throw from anywhere so it must support unwinding from any point. So if you were really intent on abusing the finally you could wrap the try-finally in a try-catch and then throw from the finally and put your continue in the catch.
The finally behave slightly different in CIL. You have protected regions and finally/fault/catch/filters handlers attached. So in order to support continue inside finally you should introduce some state machine , which is complication and generally against Roslyn design limitation.
ziml77's point is about the behaviour of the C# language. You seem to be talking about implementation concerns. They're not relevant.
That's not just Java and there is nothing really cursed about it: throwing in a finally block is the most common example. Jump statements are no different, you can't just ignore them when they override the return or throw statements.
It is just Java as far as I can tell. Other languages with a finally don't allow for explicitly exiting the finally block.
And JavaScript .. And Python (though as sibling posts have mentioned it looks like they're intending to make a breaking change to remove it).
EDIT: actually, the PEP points out that they intend for it to only be a warning in CPython, to avoid the breaking change
Notably, C++ and similar languages don't support lexical `finally` at all, instead relying on destructors, which are a function and obviously cannot affect the control flow of their caller ...
except by throwing exceptions, which is a different problem that there's no "good" solution to (during unwinding, that is).
I thought destructors were all noexcept now... or at the very least if you didn't noexcept, and then threw something, it just killed the process.
Although, strictly speaking, they could have each exception also hold a reference to the prior exception that caused the excepting object to be destroyed. This forms an intrusive linked list of exceptions. Problem is, in C++ you can throw any value, so there isn't exactly any standard way for you to get the precursor exception, or any standard way for the language to tell the exception what its precursor was. In Python they could just add a field to the BaseException class that all throwables have to inherit from.
> I thought destructors were all noexcept now...
Destructors are noexcept by default, but that can be overridden with noexcept(false).
> or at the very least if you didn't noexcept, and then threw something, it just killed the process.
IIRC throwing out of a destructor that's marked noexcept(false) terminates the process only if you're already unwinding from something else. Otherwise the exception should be thrown "normally".
> override the return
How is this not cursed
It is Java as C# disallow this
In JDK 25, you can run this code:
This is exceedingly nasty. Well Done!
try/finally is effectively try/catch(Throwable) with copy all the code of the finally block prior to exiting the method. (Java doesn't have a direct bytecode support for 'finally')
Nothing that cursed.
It compiles to this:
Won't that particular code fail to compile in java because the return is unreachable?
Isn't this just an endless loop with extra steps?
this broke my head. I think I haven't touched Java in a while and kept thinking continue should be in a case/switch so ittook a minute to back out of that alleyway before I even got what was wrong with this.
Nice post!
A minor point:
> monitors are incompatible with coroutines
If by coroutines the author meant virtual threads, then monitors have always been compatible with virtual threads (which have always needed to adhere to the Thread specification). Monitors could, for a short while, degrade the scalability of virtual threads (and in some situations even lead to deadlocks), but that has since been resolved in JDK 24 (https://openjdk.org/jeps/491).
I think it's coroutines as in other JVM languages like Kotlin, where yielding may be implemented internally as return (due to lack of native coroutine support in JVM).
Holding a lock/monitor across a yield is a bad idea for other reasons, so it shouldn't be a big deal in practice.
Older versions of Java did try to have only one copy of the finally block code. To implement this, there were "jsr" and "ret" instructions, which allowed a method (a subroutine) to contain subroutines inside it. This even curseder implementation of finally is prohibited starting from version 51 class files (Java 7).
Doesn't JRE has some limited form of decompilation in its JIT, as a pre-pass? IIRC, it reconstructs the basic blocks and CFG from the bytecode and does some minor optimizations before going on to regalloc and codegen.
It's hard to call it decompilation as opposed to just regular compilation though.