Error handling, blocks and rescue
block, rescue and always, ignore_errors and failed_when, asserting prerequisites, retries and timeouts, and making a failed run safe to repeat.
block, rescue and always
- name: Deploy with a rollback path
block:
- name: Stop the service
ansible.builtin.systemd:
name: app
state: stopped
- name: Copy the new artefact
ansible.builtin.copy:
src: "app-{{ app_version }}.jar"
dest: /srv/app/app.jar
owner: app
mode: "0640"
- name: Start the service
ansible.builtin.systemd:
name: app
state: started
- name: Wait until the health endpoint answers
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
register: health
until: health.status == 200
retries: 12
delay: 5
rescue:
- name: Restore the previous artefact
ansible.builtin.copy:
src: /srv/app/app.jar.previous
dest: /srv/app/app.jar
remote_src: true
mode: "0640"
- name: Start the known-good version
ansible.builtin.systemd:
name: app
state: restarted
- name: Fail the run so a human looks at it
ansible.builtin.fail:
msg: "Deployment of {{ app_version }} failed; rolled back to the previous build."
always:
- name: Always leave a record of the attempt
ansible.builtin.copy:
content: |
version={{ app_version }}
at={{ ansible_date_time.iso8601 }}
host={{ inventory_hostname }}
dest: /var/log/app/last-deploy.txt
mode: "0644"- A
rescueruns only for the host whose block failed, and only for tasks inside that block. It is per-host error handling, not global. alwaysruns regardless of outcome, which is where cleanup and reporting belong.- Re-raising with
failin the rescue is deliberate: a run that silently recovered hides an incident from the pipeline. - Every task in a block must be safe to run again, because a rescue does not undo a partial change on a host that failed midway.
ignore_errors, failed_when and assertions
- name: Check a prerequisite without failing the play
ansible.builtin.command: /usr/local/bin/app --version
register: app_version
failed_when: false
changed_when: false
- name: Assert the prerequisites in one place
ansible.builtin.assert:
that:
- ansible_memtotal_mb >= 4096
- app_version.stdout is version('2.0.0', '>=')
- ansible_distribution in ['Ubuntu', 'Debian']
fail_msg: >-
{{ inventory_hostname }} does not meet the requirements:
memory {{ ansible_memtotal_mb }}MB, version {{ app_version.stdout | default('missing') }}
success_msg: "prerequisites satisfied"
quiet: true
- name: Try a task that may legitimately fail
ansible.builtin.command: /usr/local/bin/app --migrate
register: migrate
# 4 means "already applied", which is a success for this task
failed_when: migrate.rc not in [0, 4]
changed_when: migrate.rc == 0| Technique | Effect | Prefer instead |
|---|---|---|
ignore_errors: true | Continue as if nothing happened | Register and check failed_when |
failed_when: false | Never fail the task | A read-only probe |
assert | Fail with a clear message | Silent failure deep in a task |
meta: end_host | Stop this host, keep the play going | Failing the whole play |
max_fail_percentage | Abort when too many hosts fail | Rolling out to a broken fleet |
ignore_errors is almost always the wrong tool because it discards the failure instead of expressing what counts as success. A registered result with an explicit failed_when says the same thing and keeps the failure visible in the report.
Making a failed run safe to repeat
- name: A migration that is safe to run twice
block:
- name: Check whether the migration already ran
ansible.builtin.stat:
path: /var/lib/app/.migrated-{{ migration_version }}
register: marker
- name: Run the migration once
ansible.builtin.command: /usr/local/bin/app migrate --to {{ migration_version }}
when: not marker.stat.exists
notify: Restart app
- name: Leave the marker only after success
ansible.builtin.file:
path: /var/lib/app/.migrated-{{ migration_version }}
state: touch
mode: "0644"
when: not marker.stat.exists
rescue:
- name: Report a partial migration without pretending it completed
ansible.builtin.fail:
msg: "Migration {{ migration_version }} failed on {{ inventory_hostname }}; the marker was not written."⚠️
A marker written before the work completes is worse than no marker: the next run skips a migration that never finished. Write the marker only after the operation succeeds, and make the operation itself idempotent where you can.
FAQ
Does rescue undo the tasks that already ran?
No. It only runs your recovery tasks. Any change made before the failure is still in place on that host, so recovery tasks must account for a partially applied state.
Where should I put prerequisite checks?
In a dedicated play or a role called first, with a single
assert block. Failing at the start, with a message that names the host and the missing requirement, is far cheaper than failing halfway through a deploy.Related
Conditionals, loops and handlers in depth Testing automation: lint, check mode and Molecule
Last refreshed 2026-09-18.