Introduction
By default, Ansible stops when a task fails. Error handling lets you control that behavior — retry, ignore, rescue, or fail fast. Use block/rescue/always (like try/catch/finally), ignore_errors, failed_when, and any_errors_fatal to build resilient playbooks.
block / rescue / always
- name: Deploy with rollback
block:
- name: Deploy new version
ansible.builtin.shell: /opt/deploy.sh {{ version }}
- name: Run smoke tests
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
rescue:
- name: Rollback on failure
ansible.builtin.shell: /opt/rollback.sh
- name: Notify team
ansible.builtin.uri:
url: "{{ slack_webhook }}"
method: POST
body_format: json
body:
text: "Deploy failed on {{ inventory_hostname }}, rolled back"
always:
- name: Clean up temp files
ansible.builtin.file:
path: /tmp/deploy-artifacts
state: absent
- name: Log result
ansible.builtin.shell: echo "Deploy attempt at $(date)" >> /var/log/deploys.log
- block — tasks to try
- rescue — runs only if block fails (like
catch) - always — runs regardless (like
finally)
ignore_errors
# Continue even if task fails
- name: Stop old service (might not exist)
ansible.builtin.service:
name: old-myapp
state: stopped
ignore_errors: true
# Check result later
- name: Check if app exists
ansible.builtin.command: which myapp
register: app_check
ignore_errors: true
- name: Install if missing
ansible.builtin.shell: curl -sL https://install.example.com | bash
when: app_check.rc != 0
failed_when
Custom failure conditions — override what Ansible considers a failure:
# Command returns rc=1 for "needs update" (not an error)
- name: Check for updates
ansible.builtin.command: /opt/app/check-updates
register: update_check
failed_when: update_check.rc > 1 # Only fail on rc > 1
# Fail based on output content
- name: Run health check
ansible.builtin.shell: /opt/app/healthcheck
register: health
failed_when: "'CRITICAL' in health.stdout"
# Multiple conditions
- name: Validate config
ansible.builtin.command: /opt/app/validate-config
register: validate
failed_when:
- validate.rc != 0
- "'WARNING' not in validate.stderr" # Warnings are OK
changed_when
Control when Ansible reports a task as "changed":
# Read-only command — never changed
- name: Check version
ansible.builtin.command: /opt/app/version
register: version
changed_when: false
# Changed only when output indicates it
- name: Apply database migration
ansible.builtin.command: /opt/app/migrate
register: migrate
changed_when: "'Applied' in migrate.stdout"
any_errors_fatal
Stop ALL hosts immediately when any single host fails:
---
- name: Critical deployment
hosts: webservers
any_errors_fatal: true # If one host fails, stop everything
tasks:
- name: Deploy
ansible.builtin.shell: /opt/deploy.sh
- name: Verify
ansible.builtin.uri:
url: "http://{{ ansible_host }}:8080/health"
Without any_errors_fatal, Ansible removes the failed host and continues with the rest. With it, the entire play stops.
max_fail_percentage
Allow partial failure:
---
- name: Rolling update
hosts: webservers
serial: 5
max_fail_percentage: 20 # Stop if >20% of batch fails
tasks:
- name: Deploy
ansible.builtin.shell: /opt/deploy.sh
Retry with until
- name: Wait for service
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
register: result
until: result.status == 200
retries: 30 # Try 30 times
delay: 10 # Wait 10 seconds between tries
Practical Patterns
Graceful Service Restart
- name: Restart with health check
block:
- name: Restart service
ansible.builtin.service:
name: myapp
state: restarted
- name: Wait for healthy
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
retries: 12
delay: 5
register: health
until: health.status == 200
rescue:
- name: Service failed to restart
ansible.builtin.debug:
msg: "WARNING: {{ inventory_hostname }} failed health check after restart"
- name: Try starting again
ansible.builtin.service:
name: myapp
state: started
Pre-Flight Validation with Bail-Out
---
- name: Deploy application
hosts: appservers
any_errors_fatal: true
tasks:
- name: Pre-flight checks
block:
- name: Check disk space
ansible.builtin.shell: df / --output=avail | tail -1
register: disk
failed_when: disk.stdout | int < 5242880
- name: Check database connectivity
ansible.builtin.command: pg_isready -h {{ db_host }}
changed_when: false
- name: Check required services
ansible.builtin.command: systemctl is-active {{ item }}
loop: [nginx, redis]
changed_when: false
rescue:
- name: Pre-flight failed
ansible.builtin.fail:
msg: "Pre-flight checks failed on {{ inventory_hostname }}. Aborting all deployments."
Ignore Individual Items in a Loop
- name: Stop services (some may not exist)
ansible.builtin.service:
name: "{{ item }}"
state: stopped
loop:
- old-app
- legacy-worker
- temp-service
ignore_errors: true
# Or check first
- name: Check services
ansible.builtin.command: systemctl status {{ item }}
loop: [old-app, legacy-worker]
register: service_status
ignore_errors: true
changed_when: false
- name: Stop only existing services
ansible.builtin.service:
name: "{{ item.item }}"
state: stopped
loop: "{{ service_status.results }}"
when: item.rc == 0
Nested Blocks
- block:
- name: Main deployment
block:
- name: Deploy code
ansible.builtin.shell: /opt/deploy.sh
- name: Run migrations
ansible.builtin.shell: /opt/migrate.sh
rescue:
- name: Rollback code
ansible.builtin.shell: /opt/rollback.sh
- name: Verify deployment
ansible.builtin.uri:
url: http://localhost:8080/health
rescue:
- name: Full failure — notify
ansible.builtin.debug:
msg: "Complete deployment failure on {{ inventory_hostname }}"
Summary Table
| Technique | Use When |
|---|---|
ignore_errors: true | Task failure is acceptable |
failed_when | Custom failure condition |
changed_when | Custom change detection |
block/rescue/always | Error recovery / rollback |
any_errors_fatal | Stop ALL hosts on any failure |
max_fail_percentage | Allow partial failure in rolling updates |
until/retries/delay | Retry until condition met |
assert | Pre-flight validation |
Related Articles
- Ansible block Module
- Ansible assert Module
- Ansible register Variables
- Ansible Playbook Guide
- Ansible when Conditional
Conclusion
Error handling makes playbooks production-ready. Use block/rescue/always for deploy-and-rollback patterns. Use ignore_errors when failure is acceptable. Use failed_when to customize what counts as failure. Use any_errors_fatal for critical operations where partial success is worse than stopping. Use until/retries for eventual consistency. Combine them to build resilient automation that handles real-world failure gracefully.