Skip to main content
RunBook Academy

AnsibleXXIII · Tags, Blocks and Error HandlingError handling

Why ignore_errors: true hides outages

Advanced⏱ ~21 minansible-playbook

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

Not yet marked complete on this device.

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:

Read-only / Safethe keyword, as documented by the installed version
$ ansible-doc -t keyword ignore_errors ignore_unreachable
ignore_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.

Read-only / Safeignore_errors against a host that cannot be reached
$ ansible-playbook -i inventory.ini unreach.yml
TASK [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=0

after 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:

Read-only / Safethe same unreachable host, with ignore_unreachable
$ ansible-playbook -i inventory.ini unreach2.yml
TASK [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=1

Read 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
Read-only / Safethe threshold firing
$ ansible-playbook -i inventory.ini reach.yml
TASK [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

Read-only / Safeevery ignore in the repository, with context
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

  1. Q1. A task carries ignore_errors: true. The host cannot be reached over SSH. What does the run do?

  2. 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.

  3. Q3. An ignore_errors written at play level applies to tasks added to that play years afterwards by people who never saw the line.

  4. 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.