AnsibleXII · Idempotency and Change ReportingIdempotency and change reporting
failed_when: when non-zero is not a failure
What you'll learn
- Express an accurate failure condition for a command with meaningful exit codes
- State why failed_when is preferable to ignore_errors for a known non-failure
- Detect a command that exits zero while having failed
- Read rescued and ignored in the recap as evidence of real failures
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
Ansible’s default rule for a command is simple and frequently wrong: exit code 0 is success, anything else is failure.
That rule matches how most tools behave and mismatches how a useful
minority behave. grep returns 1 when it found nothing, which is an
answer rather than a fault. A compliance scanner may return 2 for
“findings present”, which is the entire reason you ran it.
systemctl is-active returns 3 for “inactive”, which may be the state
you are checking for.
failed_when is how you tell Ansible what failure means for a specific
command. It is the mirror of changed_when, and it has one use case
that is more important than all the rest.
Replacing the default rule
The shape is the same as changed_when: an expression evaluated on the
controller after the module returns, whose result replaces the module’s
failed value.
- name: Check whether the deprecated setting is still present
ansible.builtin.command: grep -q '^legacy_mode' /etc/app/app.conf
register: legacy
changed_when: false
failed_when: legacy.rc not in [0, 1]
Read the three lines together, because they are one statement:
changed_when: false— grep reads, it does not write.failed_when: legacy.rc not in [0, 1]— 0 means found, 1 means not found, and both are answers. Anything else — 2 is grep’s “file could not be read” — is a genuine fault.
The not in [...] form is the one to reach for. It states the set of
codes you understand, so a code you have never seen still fails loudly.
failed_when: legacy.rc == 2 would treat a hypothetical rc 3 as
success, which is a promise you did not mean to make.
Afterwards, legacy.rc == 0 is a usable boolean for a later when:.
The dangerous inverse: exit 0 while having failed
Everything above is about a command that reports failure when it did not fail. The opposite case is rarer, harder to spot and much more expensive.
Some tools exit 0 unconditionally. Wrapper scripts that end with an
echo. Vendor CLIs that print ERROR: to stdout and exit 0 because
their author never thought about it. Anything piped through tee.
Anything ending in || true.
For those, Ansible’s default rule reports success on a failed operation, and the run goes green:
- name: Push the configuration to the appliance
ansible.builtin.command: /opt/vendor/bin/push-config --file /etc/vendor/app.conf
register: push
changed_when: "'applied' in push.stdout"
failed_when: >-
push.rc != 0
or 'ERROR' in push.stdout
or 'failed' in push.stderr
Now the task fails when the tool says it failed, regardless of what it
told the shell. This is the case where failed_when is not a
convenience — it is the only thing standing between you and a green run
over a broken change.
Writing the condition
Three rules that hold up.
Enumerate what you accept, not what you reject. rc not in [0, 1]
survives a tool that gains a new exit code; rc == 2 does not.
A list is joined with and. Same as changed_when — the keyword is
typed as a list and a list is a conjunction:
$ ansible-playbook -i inv2.ini semantics.ymlTASK [failed_when also ANDs a list] ********************************************
ok: [localhost] failed_when:
- c.rc != 0 # true
- "'nosuchtext' in c.stderr" # false -> and -> not failed
A task that failed on rc and passed on the message did not fail. That is often exactly what you want — “fail only when it exits non-zero and says something we recognise as fatal” — and it is a trap when you meant “or”.
failed_when: false is ignore_errors with extra steps. If the
expression is a constant false, you have not specified anything; you
have suppressed everything. Write what you mean, and if what you mean is
“nothing here can fail”, ask whether the task should be there at all.
Reading the consequences in the recap
$ ansible-playbook -i inv2.ini vocab.ymlTASK [A non-zero exit that is not a failure] ***********************************
ok: [localhost]
TASK [A real failure that was swallowed] ***************************************
fatal: [localhost]: FAILED! => {"changed": false, "cmd": ["/bin/false"], "msg": "The command exited with a non-zero return code.", "rc": 1}
...ignoring
TASK [Fail inside the block] ***************************************************
fatal: [localhost]: FAILED! => {"changed": false, "cmd": ["/bin/false"], "msg": "The command exited with a non-zero return code.", "rc": 1}
TASK [Recover] *****************************************************************
ok: [localhost] => {
"msg": "recovered"
}
PLAY RECAP *********************************************************************
localhost : ok=6 changed=1 unreachable=0 failed=0 skipped=1 rescued=1 ignored=1Note the difference between the first and second tasks. Both ran
/bin/false. The first, carrying failed_when: false, produced a
silent ok with no fatal: line at all. The second, carrying
ignore_errors: true, printed the full failure and then ...ignoring.
ignore_errors is at least visible — it leaves the failure in the log
and increments ignored. failed_when: false erases it entirely: no
message, no counter, nothing. Of the two bad options, the loud one is
less bad.
Knowledge check
Knowledge check · 4 questions
Q1. A grep task is used to test whether a setting is present. Which failure condition is the most robust?
Q2. A vendor CLI prints ERROR: authentication rejected to stdout and exits 0. With no keywords, what does Ansible report?
Q3. What distinguishes failed_when from ignore_errors? Select all that apply.
Q4. A task can report changed: true and FAILED! at the same time.
Passing score: 75%. Answers are checked in this browser.