Ansible ignore_errors: Skip Task Failures & Continue Playbook (Guide)
By Luca Berton · Published 2024-01-01 · Category: installation
How to handle errors in Ansible with ignore_errors, failed_when, block/rescue/always. Control task failure behavior, continue on error.
When an Ansible task fails, the entire playbook stops for that host. Sometimes that's what you want. Sometimes you need to continue — clean up after failures, try alternatives, or skip optional steps. Here's every way to handle errors in Ansible.
ignore_errors: true
The simplest approach — the task can fail and the playbook continues:
- name: Check if old service exists (may not be installed)
ansible.builtin.command: systemctl status legacy-app
register: legacy_status
ignore_errors: true
- name: Stop old service if it was running
ansible.builtin.service:
name: legacy-app
state: stopped
when: legacy_status.rc == 0
⚠️ The problem with ignore_errors: It swallows ALL failures, including unexpected ones. Your playbook reports "ok" even when something is genuinely broken.
See also: Ansible ignore_errors: Error Handling Best Practices (Complete Guide)
failed_when: Custom Failure Conditions
More precise than ignore_errors — define exactly what counts as failure:
- name: Check disk space
ansible.builtin.command: df -h /data
register: disk_check
failed_when: "'100%' in disk_check.stdout"
- name: Run database query (exit code 1 means "no rows", not error)
ansible.builtin.command: /opt/db/check-pending-jobs.sh
register: job_check
failed_when: job_check.rc not in [0, 1]
# rc=0: jobs found, rc=1: no jobs (ok), rc=2+: actual error
Common failed_when Patterns
# Fail only on specific return codes
failed_when: result.rc not in [0, 2]
# Fail if output contains error message
failed_when: "'ERROR' in result.stdout"
# Fail if command took too long (indicates problem)
failed_when: result.delta | regex_search('[0-9]+') | int > 30
# Never fail (same as ignore_errors, but more explicit)
failed_when: false
# Multiple conditions
failed_when:
- result.rc != 0
- "'already exists' not in result.stderr"
changed_when: Control Change Status
Not error handling per se, but often paired with it:
- name: Check if reboot is needed
ansible.builtin.command: needs-restarting -r
register: reboot_check
changed_when: reboot_check.rc == 1
failed_when: reboot_check.rc > 1
See also: Ansible ignore_errors: Handle Task Failures (Complete Guide)
block/rescue/always: Try-Catch
The most powerful pattern — Ansible's equivalent of try/catch/finally:
- name: Deploy with rollback
block:
- name: Deploy new version
ansible.builtin.copy:
src: "app-v{{ new_version }}.tar.gz"
dest: /opt/app/
- name: Extract and install
ansible.builtin.unarchive:
src: "/opt/app/app-v{{ new_version }}.tar.gz"
dest: /opt/app/current/
remote_src: true
- name: Run health check
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
retries: 5
delay: 10
rescue:
- name: Deployment failed — rolling back
ansible.builtin.debug:
msg: "⚠️ Deployment failed on {{ inventory_hostname }}, rolling back..."
- name: Restore previous version
ansible.builtin.copy:
src: "/opt/app/backup/app-v{{ current_version }}.tar.gz"
dest: /opt/app/
- name: Restart with old version
ansible.builtin.service:
name: myapp
state: restarted
always:
- name: Clean up temp files
ansible.builtin.file:
path: /tmp/deploy-staging
state: absent
- name: Send notification
ansible.builtin.uri:
url: "https://hooks.slack.com/services/xxx"
method: POST
body_format: json
body:
text: "Deploy {{ new_version }} on {{ inventory_hostname }}: {{ 'FAILED (rolled back)' if ansible_failed_task is defined else 'SUCCESS' }}"
ignore_unreachable
For hosts that might be offline:
- name: Check all servers (some may be down)
hosts: all
ignore_unreachable: true
tasks:
- name: Ping
ansible.builtin.ping:
register: ping_result
- name: Only run on reachable hosts
ansible.builtin.debug:
msg: "{{ inventory_hostname }} is alive"
when: ping_result is not failed
See also: Ansible block, rescue, always: Error Handling Complete Guide (2026)
any_errors_fatal
The opposite of ignore_errors — fail the ENTIRE play if ANY host fails:
- name: Critical database migration
hosts: dbservers
any_errors_fatal: true # If one DB fails, stop ALL hosts
tasks:
- name: Run migration
ansible.builtin.command: /opt/db/migrate.sh
max_fail_percentage
Stop the play if too many hosts fail:
- name: Rolling update
hosts: webservers
serial: 5
max_fail_percentage: 20 # Stop if >20% of batch fails
tasks:
- name: Deploy
ansible.builtin.copy:
src: app.tar.gz
dest: /opt/app/
Pattern: Optional Task
# Only run if the tool exists
- name: Clear application cache (if artisan exists)
ansible.builtin.command: php artisan cache:clear
args:
chdir: /var/www/app
register: cache_clear
failed_when:
- cache_clear.rc != 0
- "'not found' not in cache_clear.stderr"
Pattern: First Available
- name: Try multiple package managers
block:
- name: Try apt
ansible.builtin.apt:
name: nginx
state: present
rescue:
- name: Try dnf
ansible.builtin.dnf:
name: nginx
state: present
Pattern: Validate Before and After
- name: Safe config update
block:
- name: Backup current config
ansible.builtin.copy:
src: /etc/nginx/nginx.conf
dest: /etc/nginx/nginx.conf.bak
remote_src: true
- name: Deploy new config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
- name: Test config
ansible.builtin.command: nginx -t
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
rescue:
- name: Restore backup
ansible.builtin.copy:
src: /etc/nginx/nginx.conf.bak
dest: /etc/nginx/nginx.conf
remote_src: true
- name: Reload with old config
ansible.builtin.service:
name: nginx
state: reloaded
- name: Fail with clear message
ansible.builtin.fail:
msg: "Nginx config update failed — rolled back to previous version"
Comparison Table
| Directive | Scope | Use Case |
|-----------|-------|----------|
| ignore_errors: true | Single task | Optional tasks, skip non-critical failures |
| failed_when | Single task | Custom failure conditions |
| block/rescue/always | Group of tasks | Try/catch/finally pattern |
| ignore_unreachable | Play or task | Handle offline hosts |
| any_errors_fatal | Play | Critical operations — stop everything on first failure |
| max_fail_percentage | Play | Rolling updates — abort if too many fail |
FAQ
When should I use ignore_errors vs failed_when?
Use failed_when whenever possible — it's more precise. ignore_errors: true hides all failures; failed_when lets you define exactly what constitutes a real failure vs expected behavior. Example: a command returning exit code 1 might mean "not found" (acceptable) vs exit code 2 meaning actual error.
Does ignore_errors affect handlers?
No. If a task with ignore_errors: true also has notify, the handler is NOT notified when the task fails. The task must succeed to trigger handlers.
Can I use block/rescue across multiple plays?
No. block/rescue/always works within a single play. For cross-play error handling, use meta: end_play or set facts that subsequent plays check.
How do I make a playbook continue but report failures at the end?
Register each risky task's result, continue with ignore_errors, then fail at the end:
- name: Risky task
ansible.builtin.command: /opt/check.sh
register: check_result
ignore_errors: true
# ... more tasks ...
- name: Fail if any checks failed
ansible.builtin.fail:
msg: "Check failed: {{ check_result.stderr }}"
when: check_result is failed
Conclusion
Start with failed_when for precise failure control. Use block/rescue/always for operations that need rollback. Reserve ignore_errors: true for truly optional tasks. And use any_errors_fatal for critical operations where one failure means everything should stop.
Related Articles
• Ansible Check Mode and Diff Mode • Ansible debug and assert Guide • Ansible Idempotent Playbooks • Ansible block/rescue/always GuideCategory: installation