Error Handling¶
What You'll Learn¶
- What Ansible does by default when a task fails on one host
- How to define failure precisely with
failed_whenandchanged_when - When
ignore_errorsis acceptable, and what it doesn't cover - How
any_errors_fatalandmax_fail_percentagestop a whole rollout early
Why This Exists¶
By default, a failed task on one host removes that host from the rest of the play but lets other hosts continue — that default is usually right, but not always, and this page covers the keywords that change it deliberately.
Mental Model¶
Ask two separate questions. "What counts as failure for this task?" — answered per task with
failed_whenorignore_errors. "How many failed hosts should stop everything?" — answered per play withany_errors_fatalandmax_fail_percentage.
| Scope | Keyword | Effect |
|---|---|---|
| Task | failed_when |
Redefine failure from the result |
| Task | changed_when |
Redefine "changed" from the result |
| Task | ignore_errors |
Keep going on this host after a failure |
| Task | ignore_unreachable |
Keep going if the host is unreachable for this task |
| Play | any_errors_fatal |
One failed host stops the play for all hosts |
| Play | max_fail_percentage |
Stop once too many hosts in a batch fail |
Defining Failure: failed_when¶
command and shell fail on any non-zero exit code. Many tools use exit codes differently:
- name: Check for pending schema migrations
ansible.builtin.command: /opt/checkout/bin/migrate --status
register: migrate_status
changed_when: false
# exit 0 = up to date, exit 2 = migrations pending, anything else = real error
failed_when: migrate_status.rc not in [0, 2]
- name: Run pending migrations
ansible.builtin.command: /opt/checkout/bin/migrate
when: migrate_status.rc == 2
Combine conditions, including checks on output:
- name: Validate the nginx configuration
ansible.builtin.command: nginx -t
register: nginx_test
changed_when: false
failed_when:
- nginx_test.rc != 0
- "'test is successful' not in nginx_test.stderr"
A list under failed_when means all conditions must be true (logical AND). Use or in a single expression for OR.
Defining Change: changed_when¶
Read-only commands should never report changed, or every run looks like it modified the host:
- name: Read the installed version
ansible.builtin.command: /opt/checkout/bin/checkout --version
register: installed_version
changed_when: false
- name: Rotate the API key only if the tool says it did something
ansible.builtin.command: /opt/checkout/bin/rotate-key
register: rotate
changed_when: "'rotated' in rotate.stdout"
This is what keeps command-based tasks compatible with Idempotency checks.
ignore_errors: Rarely the Right First Instinct¶
- name: Stop the legacy service if it happens to exist
ansible.builtin.service:
name: legacy-checkout
state: stopped
register: legacy_stop
ignore_errors: true
- name: Report that the legacy service wasn't present
ansible.builtin.debug:
msg: "legacy-checkout not installed on {{ inventory_hostname }}"
when: legacy_stop is failed
The task still shows as failed (with ...ignoring) and counts in the recap's ignored= column. Two limits:
- It hides every failure of that task, including ones you didn't anticipate (a typo in the service name looks identical to "not installed").
- It doesn't cover unreachable hosts or syntax/undefined-variable errors. Use
ignore_unreachable: truefor the former.
Prefer failed_when whenever you can describe the acceptable outcome precisely.
Failing on Purpose: assert and fail¶
- name: Refuse to deploy without a release version
ansible.builtin.assert:
that:
- app_release is defined
- app_release is match('^[0-9]+\.[0-9]+\.[0-9]+$')
fail_msg: "app_release must be set to a semantic version, got: {{ app_release | default('undefined') }}"
quiet: true
Preflight assertions at the top of a play fail fast with a clear message, before anything changes.
Stopping a Whole Rollout¶
any_errors_fatal¶
- name: Update the database cluster
hosts: db
any_errors_fatal: true
tasks:
- name: Apply cluster-wide configuration
ansible.builtin.template:
src: postgresql.conf.j2
dest: /etc/postgresql/16/main/postgresql.conf
The moment any host fails, Ansible finishes the current task on the other hosts and then ends the play for all of them. Use it when a partial change is worse than no change — clustered databases, consensus systems.
max_fail_percentage¶
- name: Rolling update of 20 web servers
hosts: web
serial: 5
max_fail_percentage: 20
tasks:
- name: Deploy new release
ansible.builtin.include_role:
name: checkout
The percentage is evaluated per batch. In a batch of 5, one failure is exactly 20%, which does not abort; the play aborts only when failures exceed the threshold (two failures, 40%). With max_fail_percentage: 0, any failure stops the rollout.
This pairs naturally with the stepped serial pattern in Case Study: Rolling Nginx Deployment.
Ending a Host or Play Cleanly¶
- name: Skip hosts that are already on the target release
ansible.builtin.meta: end_host
when: running_release == app_release
meta: end_host stops the play for that host without marking it failed. meta: end_play does the same for every host.
How These Interact With block/rescue¶
A task failure inside a block with a rescue doesn't count toward max_fail_percentage if rescue recovers the host. If you want a rescued rollback to still stop the rollout, end rescue with ansible.builtin.fail.
Common Mistakes¶
- Reaching for
ignore_errors: trueto silence a failure instead of fixing the underlyingfailed_whenlogic — this hides real problems, not just cosmetic ones. - Not setting
any_errors_fatal/max_fail_percentageon a rollout that should stop early if early hosts are already failing. - Forgetting
changed_when: falseon read-onlycommandtasks, so every run reports changes. - Assuming
max_fail_percentage: 20stops a 5-host batch after one failure — it has to be exceeded, not reached. - Expecting
ignore_errorsto keep going when a host becomes unreachable.
Interview Questions¶
- What's the difference between
ignore_errorsandfailed_when? - How would you stop an entire rollout early if 20% of hosts fail?
- A
commandtask reportschangedon every run. How do you fix it without changing the command? - When would you choose
any_errors_fatalovermax_fail_percentage?
Next¶
Continue to Imports vs. Includes.