Skip to main content
RunBook Academy

AnsibleXII · Idempotency and Change ReportingIdempotency and change reporting

failed_when: when non-zero is not a failure

Intermediate⏱ ~17 minansible-playbook

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

Not yet marked complete on this device.

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:

Read-only / Safefailed_when list semantics
$ ansible-playbook -i inv2.ini semantics.yml
TASK [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

Read-only / Safethree failures, none of them counted as failed
$ ansible-playbook -i inv2.ini vocab.yml
TASK [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=1

Note 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

  1. Q1. A grep task is used to test whether a setting is present. Which failure condition is the most robust?

  2. Q2. A vendor CLI prints ERROR: authentication rejected to stdout and exits 0. With no keywords, what does Ansible report?

  3. Q3. What distinguishes failed_when from ignore_errors? Select all that apply.

  4. Q4. A task can report changed: true and FAILED! at the same time.

Passing score: 75%. Answers are checked in this browser.