Skip to main content
RunBook Academy

AnsibleXXIII · Tags, Blocks and Error HandlingError handling

Overriding the module verdict

Intermediate⏱ ~18 minansible-playbook

What you'll learn

  • State what a list of conditions means, and express an or correctly
  • Choose failed_when over ignore_errors for a known-acceptable failure
  • Keep a command task honest about whether it changed anything
  • Recognise a failed_when that silences rather than describes

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.

Part XII established these two keywords as instruments of honest reporting: changed_when supplies knowledge a command module cannot obtain, and failed_when expresses what failure actually means for a command that uses exit codes to communicate rather than to complain.

This lesson takes them up again from the error-handling side, because in this part they are the alternative to the thing lesson 7 argues against. When somebody reaches for ignore_errors: true because “that task fails on hosts where the package is already there”, the correct answer is nearly always a failed_when that says which failure is acceptable.

The difference in one line:

  • ignore_errors: trueI do not want to hear about failures here.
  • failed_when: <condition>this specific outcome is not a failure; every other outcome still is.

The first discards a distinction. The second states one.

The implicit and

A failed_when may be a string or a list. A list is joined with an implicit and, which is the single most consequential detail in this lesson.

- name: create the reporting user
  ansible.builtin.command: /usr/local/bin/create-user reporting
  register: create_user
  changed_when: "'created' in create_user.stdout"
  failed_when:
    - create_user.rc != 0
    - "'already exists' not in create_user.stdout"

Read that as: fail if the exit code is non-zero and the output does not say the user already exists. Which is exactly right — it accepts one specific benign non-zero exit and fails on everything else.

Read-only / Safethe list form, with rc=1 and 'already exists' in the output
$ ansible-playbook -i inventory.ini failedwhen.yml
TASK [list of conditions - joined with and] ************************************
ok: [localhost] => {
  "msg": "list-form"
}

Now the mistake. Somebody wants fail if the exit code is bad or the output mentions a fatal error, and writes it the same way:

  failed_when:
    - probe.rc not in [0, 1]
    - "'fatal' in probe.stdout"

That does not mean “or”. It means the task fails only when both hold — an unexpected exit code and the word fatal in the output. A run with rc: 3 and a clean stdout passes. A genuine failure is reported as success, on every host, for as long as the playbook exists.

Multi-condition failure logic needs a single string with an explicit operator:

  failed_when: probe.rc not in [0, 1] or 'fatal' in probe.stdout
Read-only / Safethe single-string form with an explicit or
$ ansible-playbook -i inventory.ini failedwhen.yml
TASK [single string with an explicit or] ***************************************
ok: [localhost] => {
  "msg": "or-form"
}

changed_when behaves identically — a list is joined with and — and when does too. That consistency is the reason for the design and the reason the trap is easy to walk into: the list form is correct and idiomatic on when, where “run this only if all of these hold” is usually what you want.

failed_when as the replacement for ignore_errors

A concrete before and after.

# Before: the task failed on three hosts once, so somebody silenced it.
- name: register the node with the inventory service
  ansible.builtin.command: /usr/local/bin/register-node
  ignore_errors: true

This now hides everything: the service being down, the credential having expired, the binary having been removed by a package upgrade. The play reports success through all of it.

# After: state which outcome is acceptable.
- name: register the node with the inventory service
  ansible.builtin.command: /usr/local/bin/register-node
  register: register_node
  changed_when: "'registered' in register_node.stdout"
  failed_when: >-
    register_node.rc != 0 and 'already registered' not in register_node.stdout

Now a node that was already registered passes, and a node that could not reach the service fails — which is the distinction the original author actually had in mind and never wrote down.

The cost of the second version is that you had to find out what the command returns in the benign case. That is the work ignore_errors lets you skip, and it is the work that makes the difference between automation you can trust and automation that reports success.

changed_when and the operational cost of lying

A command or shell task reports changed every time, because the module has no way to know whether the command changed anything. Left alone, that has two costs: the recap becomes uninformative, and — the expensive one — handlers fire on every run.

- name: check the certificate expiry
  ansible.builtin.command: openssl x509 -enddate -noout -in /etc/ssl/certs/site.pem
  register: cert_expiry
  changed_when: false

changed_when: false on a read-only command is not a cosmetic tidy-up. Part XII lesson 3 showed the full mechanism; the consequence relevant here is that a probe reporting changed will notify any handler attached to it, and a service that restarts on every run is indistinguishable from a service restarting because of a real change.

The corresponding honest version for a command that sometimes changes something:

- name: rotate the application secret if it is older than the policy allows
  ansible.builtin.command: /usr/local/bin/rotate-secret --if-older-than 90d
  register: rotate
  changed_when: "'rotated' in rotate.stdout"
  failed_when: rotate.rc != 0

The version that silences instead of describing

failed_when can be abused into exactly the thing it replaces:

- name: this is ignore_errors with extra steps
  ansible.builtin.command: /usr/local/bin/register-node
  register: r
  failed_when: false

failed_when: false means never fail. It is ignore_errors: true written so that a reviewer scanning for ignore_errors will not find it, and it carries none of the “I thought about this” that a real condition implies.

There is one legitimate use: a deliberate probe whose result you are about to inspect yourself.

- name: probe whether the service is already configured
  ansible.builtin.command: /usr/local/bin/webapp-status
  register: webapp_status
  changed_when: false
  failed_when: false

- name: configure it if the probe says it is not configured
  ansible.builtin.include_role:
    name: webapp
  when: webapp_status.rc != 0

Here the non-zero exit is data, consumed two lines later. The distinguishing question is whether anything reads the registered result. If nothing does, failed_when: false is silence.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A task carries failed_when written as a list of three conditions. What does adding a fourth condition to that list do?

  2. Q2. Which of these are honest uses of these keywords rather than silencing? Select all that apply.

  3. Q3. A recap line reading failed=3 changed=0 proves that nothing was modified on those three hosts.

  4. Q4. A colleague explains that a task carries ignore_errors: true because it fails on hosts where the resource already exists. What is the right change?

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