You can capture the exit value from the condition and propagate that forward:
while rmdir FOO; ss=$?; [[ $ss -eq 0 ]]
do
echo in loop
done
echo "out of loop with ?=$? but ss=$ss"
Output
rmdir: failed to remove 'FOO': No such file or directory
out of loop with ?=0 but ss=1
In this instance the exit status from rmdir FOO has been captured in the variable ss and is 1. (Try replacing rmdir FOO with ( exit 4 ). You'll find that ss=4.)
How does this work? Remember that the syntax is actually while list-1; do list-2; done, and not the much more usual expectation of while command; do list; done. The list-1 can be a sequence of semicolon-separated commands, and the documentation states that the "while command continuously executes the list list-2 as long as the last command in the list list-1 returns an exit status of zero."
As an alternative presentation of the messy-looking while condition, it is possible to assign a variable while inside an expression (( ... )), and then to use the result. This gives the harder-to-read but more compact assign-and-test structure:
while rmdir FOO; ((! (ss=$?)))
do
echo in loop
done
echo "out of loop with ?=$? but ss=$ss"
Alternatively you can use while rmdir FOO; ! (( ss=$? )). These work because ((1)) evaluates arithmetically to 1, which is generally associated with true, and so the exit code of that evaluation is 0 (success). On the other hand, ((0)) evaluates arithmetically to 0, which is generally associated with false, and so the exit code of that evaluation is 1 (failure). This may seem confusing, as after all both evaluations ((.)) are "successful", but this is a hack to bring the value of arithmetic expressions representing true/false in line with bash's exit codes of success/failure, and make conditional expressions like if ...; then ...; fi, while ...; do ...; done, etc, work correctly, whether based on exit codes or arithmetic values.