AnsibleXXIII · Tags, Blocks and Error HandlingError handling
Why ignore_errors: true hides outages
What you'll learn
- State exactly which failure classes ignore_errors does and does not cover
- Distinguish ignore_errors from ignore_unreachable and choose deliberately
- Read a recap that reports success through an outage
- Replace a blanket ignore with a precondition, a failed_when or a rescue
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
A deployment play carries ignore_errors: true on the package task, because
one host was flaky once, eighteen months ago, and the person who added it has
left.
The run reports success across 200 hosts. The new package installed on none of them.
This lesson is about why that line was both more dangerous and less useful than the person who added it believed, and what to write instead.
What it does not cover
The documentation says ignore_errors “only works when the task can run and
returns a value of failed”. ansible-doc -t keyword puts it more briefly:
$ ansible-doc -t keyword ignore_errors ignore_unreachableignore_errors:
applies_to:
- Play
- Role
- Block
- Task
- Handler
description: Boolean that allows you to ignore task failures and continue with play.
It does not affect connection errors.
priority: 0
template: explicit
type: bool
ignore_unreachable:
applies_to:
- Play
- Role
- Block
- Task
- Handler
description: Boolean that allows you to ignore task failures due to an unreachable
host and continue with the play. This does not affect other task errors (see `ignore_errors`)
but is useful for groups of volatile/ephemeral hosts.
priority: 0
template: explicit
type: bool“It does not affect connection errors.” That is the important sentence and it is worth seeing executed, because it produces a result that looks nothing like what the person adding the line expected.
$ ansible-playbook -i inventory.ini unreach.ymlTASK [needs a connection, ignore_errors set] ***********************************
fatal: [web-01.example.com]: UNREACHABLE! => {"changed": false, "msg": "Task failed: Failed to connect to the host via ssh: ssh: connect to host 192.0.2.11 port 22: Connection timed out", "unreachable": true}
PLAY RECAP *********************************************************************
web-01.example.com : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0after the ping — the next task in the play — does not appear at all.
ignore_errors did nothing. The host is out of the play, unreachable=1,
and the exit code is 4.
So the line somebody added to survive “a flaky host” does not survive the commonest flakiness there is. What it does cover is a module that ran and returned failure — which is the class of failure you most want to hear about.
ignore_unreachable and the recap that lies
ignore_unreachable is the separate, deliberate decision for connection
failures. It keeps the host in the play:
$ ansible-playbook -i inventory.ini unreach2.ymlTASK [needs a connection, ignore_unreachable set] ******************************
fatal: [web-01.example.com]: UNREACHABLE! => {"changed": false, "msg": "Task failed: Failed to connect to the host via ssh: ssh: connect to host 192.0.2.11 port 22: Connection timed out", "unreachable": true}
TASK [after the ping] **********************************************************
ok: [web-01.example.com]
PLAY RECAP *********************************************************************
web-01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1Read the recap: unreachable=0.
The host was never contacted. It is down, or the network is broken, or the
SSH key is wrong. The recap reports zero unreachable hosts and the failure
appears only in the ignored column, which nothing monitors.
The safer pattern for a fleet with expected absences is to find out who is reachable first, and act on the answer:
- name: establish who is reachable
hosts: all
gather_facts: false
tasks:
- name: probe the connection
ansible.builtin.ping:
register: reachability
ignore_unreachable: true
- name: name the hosts that did not answer
ansible.builtin.debug:
msg: "{{ inventory_hostname }} is unreachable and will be skipped"
when: reachability.unreachable | default(false)
- name: refuse to proceed if more than ten percent are missing
vars:
missing: >-
{{ ansible_play_hosts_all
| map('extract', hostvars, ['reachability', 'unreachable'])
| select('defined') | list }}
ansible.builtin.assert:
that: missing | length <= (ansible_play_hosts_all | length * 0.1)
fail_msg: >-
{{ missing | length }} of {{ ansible_play_hosts_all | length }}
hosts are unreachable. Refusing to proceed with a partial change.
run_once: true
$ ansible-playbook -i inventory.ini reach.ymlTASK [probe the connection] ****************************************************
fatal: [web-01.example.com]: UNREACHABLE! => {"changed": false, "msg": "Task failed: Failed to connect to the host via ssh: ssh: connect to host 192.0.2.11 port 22: Connection timed out", "unreachable": true}
TASK [name the hosts that did not answer] **************************************
ok: [web-01.example.com] => {
"msg": "web-01.example.com is unreachable and will be skipped"
}
TASK [refuse to proceed if more than ten percent are missing] ******************
fatal: [localhost]: FAILED! => {
"msg": "1 of 2 hosts are unreachable. Refusing to proceed with a partial change."
}That converts a silent absence into an explicit decision with a threshold somebody chose.
Note that the count comes from the registered results rather than from
ansible_play_hosts. ignore_unreachable keeps the host in the play, so
ansible_play_hosts does not shrink and cannot be used to detect the
absence — which is the same disconnect the recap shows, arriving through a
variable instead of a counter.
The replacement ladder
When you find ignore_errors: true in a review, work down this list. The
first option that applies is the right one.
1. A precondition that makes the failure impossible.
The task fails because a directory does not exist? Create the directory.
Because a package is missing? Install it. Most ignore_errors lines are
sitting on top of an unstated dependency of the kind Part XXII lesson 2
described, and the honest fix is to state it.
2. A failed_when that names the acceptable outcome.
- name: register the node
ansible.builtin.command: /usr/local/bin/register-node
register: reg
changed_when: "'registered' in reg.stdout"
failed_when: reg.rc != 0 and 'already registered' not in reg.stdout
Lesson 6 covers this. It accepts one specific benign result and fails on
everything else, which is what the author of the ignore_errors line
actually meant.
3. A block with a rescue that handles it.
When the failure is real and there is something sensible to do about it — restore the previous config, mark the node out of service, alert — that is a rescue, and lesson 5 covers what belongs in one.
4. ignore_errors: true with a comment saying why.
Occasionally correct. A best-effort cleanup on an ephemeral host; a notification to a system whose availability genuinely does not gate the change. When it is right, say so where the next reader will look:
- name: best-effort notification to the chat webhook
ansible.builtin.uri:
url: "{{ webapp_notify_url }}"
method: POST
body_format: json
body:
text: "deploy complete on {{ inventory_hostname }}"
# ignore_errors is deliberate: the webhook is informational and its
# availability must not gate a deployment. Reviewed 2026-08-11.
ignore_errors: true
An ignore_errors without a justification comment should not pass review.
That is a cheap rule and it works, because the act of writing the
justification is what exposes the ones that do not have one.
Finding them
grep -rn -B6 'ignore_errors\|ignore_unreachable' \
playbooks/ roles/ --include='*.yml' --include='*.yaml'Pay particular attention to any at play or role level. Both keywords
accept Play and Role in their applies_to, and a play-level
ignore_errors: true silences every task in it, including ones written years
later by people who never saw the line.
Knowledge check
Knowledge check · 4 questions
Q1. A task carries ignore_errors: true. The host cannot be reached over SSH. What does the run do?
Q2. A play uses ignore_unreachable: true and forty of two hundred hosts are down. Which statements about the resulting recap are true? Select all that apply.
Q3. An ignore_errors written at play level applies to tasks added to that play years afterwards by people who never saw the line.
Q4. You are reviewing a task with ignore_errors: true that fails only on hosts where the resource already exists. Working down the replacement ladder, what is the first option to consider?
Passing score: 75%. Answers are checked in this browser.