Ansible block, rescue, always: Error Handling Complete Guide (2026)
By Luca Berton · Published 2024-01-01 · Category: installation
Complete guide to Ansible block, rescue, and always for error handling. Implement try/catch logic, rollback failed tasks, clean up resources, and build.
Ansible's block, rescue, and always directives work like try/catch/finally in programming languages. They let you group tasks, handle errors gracefully, roll back failed changes, and ensure cleanup always runs — essential for production-grade playbooks.
Basic Syntax
- name: Handle errors with block/rescue/always
block:
# Tasks to try (like "try")
- name: Task that might fail
ansible.builtin.command: /usr/bin/risky-command
rescue:
# Tasks to run if block fails (like "catch")
- name: Handle the failure
ansible.builtin.debug:
msg: "The command failed, running recovery steps"
always:
# Tasks that always run (like "finally")
- name: Cleanup regardless of outcome
ansible.builtin.debug:
msg: "This always runs"
See also: Ansible changed_when & failed_when: Control Task Status (Guide)
block — Group Tasks Together
Even without rescue/always, block groups tasks and applies common directives:
- name: Install and configure nginx
block:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
become: true
when: install_nginx | default(true)
tags: ['nginx']
All three tasks inherit become: true, when:, and tags: from the block.
rescue — Handle Failures
The rescue section runs only when a task in block fails:
- name: Deploy with rollback
block:
- name: Pull latest code
ansible.builtin.git:
repo: https://github.com/myorg/myapp.git
dest: /opt/myapp
version: "{{ deploy_version }}"
- name: Run database migrations
ansible.builtin.command:
cmd: /opt/myapp/manage.py migrate
changed_when: true
- name: Restart application
ansible.builtin.service:
name: myapp
state: restarted
rescue:
- name: Rollback to previous version
ansible.builtin.git:
repo: https://github.com/myorg/myapp.git
dest: /opt/myapp
version: "{{ previous_version }}"
- name: Restart with previous version
ansible.builtin.service:
name: myapp
state: restarted
- name: Send failure notification
ansible.builtin.uri:
url: "{{ slack_webhook }}"
method: POST
body: '{"text": "Deploy of {{ deploy_version }} failed on {{ inventory_hostname }}, rolled back"}'
body_format: json
See also: Ansible retries & until: Retry Failed Tasks Automatically (Guide)
always — Guaranteed Cleanup
The always section runs regardless of success or failure:
- name: Database maintenance with guaranteed cleanup
block:
- name: Stop application
ansible.builtin.service:
name: myapp
state: stopped
- name: Run database vacuum
community.postgresql.postgresql_query:
db: myapp
query: "VACUUM FULL ANALYZE;"
rescue:
- name: Log failure
ansible.builtin.lineinfile:
path: /var/log/maintenance.log
line: "{{ ansible_date_time.iso8601 }} - Database maintenance FAILED"
create: true
always:
- name: Ensure application is running
ansible.builtin.service:
name: myapp
state: started
- name: Log completion
ansible.builtin.lineinfile:
path: /var/log/maintenance.log
line: "{{ ansible_date_time.iso8601 }} - Database maintenance completed"
create: true
Access Error Information in rescue
Ansible provides special variables inside rescue:
- name: Handle errors with context
block:
- name: Risky operation
ansible.builtin.command: /usr/bin/might-fail
register: cmd_result
rescue:
- name: Show which task failed
ansible.builtin.debug:
msg: |
Failed task: {{ ansible_failed_task.name }}
Failed result: {{ ansible_failed_result.msg | default('unknown') }}
Host: {{ inventory_hostname }}
- name: Different handling based on error
ansible.builtin.debug:
msg: "Handling specific error"
when: "'permission denied' in (ansible_failed_result.msg | default('') | lower)"
Available Variables in rescue
| Variable | Description |
|----------|-------------|
| ansible_failed_task | The task object that failed (name, action, etc.) |
| ansible_failed_result | The result dict from the failed task (msg, rc, stderr, etc.) |
See also: Ansible troubleshooting - Error internal-error
Nested Blocks
Blocks can be nested for complex error handling:
- name: Multi-level error handling
block:
- name: Primary database
block:
- name: Connect to primary DB
community.postgresql.postgresql_ping:
db: myapp
login_host: primary-db.example.com
rescue:
- name: Failover to secondary DB
block:
- name: Connect to secondary DB
community.postgresql.postgresql_ping:
db: myapp
login_host: secondary-db.example.com
rescue:
- name: Both databases down
ansible.builtin.fail:
msg: "CRITICAL: Both primary and secondary databases are unreachable"
Real-World Patterns
Safe Service Restart
- name: Safe config update with validation
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
validate: nginx -t -c %s
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
rescue:
- name: Restore backup config
ansible.builtin.copy:
src: /etc/nginx/nginx.conf.bak
dest: /etc/nginx/nginx.conf
remote_src: true
- name: Reload with original config
ansible.builtin.service:
name: nginx
state: reloaded
- name: Report failure
ansible.builtin.debug:
msg: "Config update failed, reverted to backup"
always:
- name: Remove backup file
ansible.builtin.file:
path: /etc/nginx/nginx.conf.bak
state: absent
Health Check After Deploy
- name: Deploy with health check
block:
- name: Deploy new version
ansible.builtin.include_role:
name: deploy
- name: Wait for health check
ansible.builtin.uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
register: health
retries: 10
delay: 5
until: health.status == 200
rescue:
- name: Health check failed — rollback
ansible.builtin.include_role:
name: rollback
- name: Notify team
ansible.builtin.mail:
to: team@example.com
subject: "Deploy FAILED on {{ inventory_hostname }}"
body: "Health check failed after deploy. Rolled back automatically."
Temporary File Cleanup
- name: Process data with temp file cleanup
block:
- name: Create temp directory
ansible.builtin.tempfile:
state: directory
register: temp_dir
- name: Download large file
ansible.builtin.get_url:
url: "{{ data_url }}"
dest: "{{ temp_dir.path }}/data.tar.gz"
- name: Extract and process
ansible.builtin.unarchive:
src: "{{ temp_dir.path }}/data.tar.gz"
dest: /opt/data/
remote_src: true
always:
- name: Clean up temp directory
ansible.builtin.file:
path: "{{ temp_dir.path }}"
state: absent
when: temp_dir.path is defined
block vs ignore_errors
| Feature | block/rescue | ignore_errors: true |
|---------|---------------|----------------------|
| Error handling | Custom recovery logic | Silently continues |
| Rollback | ✅ Yes | ❌ Manual only |
| Error info | ansible_failed_task, ansible_failed_result | Only via register |
| Cleanup | always section | Manual |
| Play status | Rescued = success | Ignored = success |
| Use when | Recovery steps needed | Error is acceptable |
Best Practices
Always usealways for cleanup — temp files, locks, service states
Don't overuse rescue — Only for real recovery logic, not suppressing errors
Log failures in rescue — Don't silently swallow errors
Test rescue paths — Force failures in staging to verify rollback works
Use ansible_failed_task — Log which specific task failed for debugging
Keep blocks focused — One logical operation per block, not entire playbooks
FAQ
What is block, rescue, always in Ansible?
They're Ansible's error handling mechanism, equivalent to try/catch/finally. block groups tasks to try, rescue runs if any block task fails, and always runs regardless of success or failure. Together they enable graceful error handling and guaranteed cleanup.
Does rescue run if any task in the block fails?
Yes, if any task in the block section fails, execution immediately jumps to rescue. Remaining block tasks are skipped. After rescue completes, the always section runs.
Can I use block without rescue?
Yes. block alone is useful for grouping tasks and applying shared directives (become, when, tags) to multiple tasks at once, even without error handling.
What happens if a task in rescue also fails?
The play fails for that host. If there's an always section, it still runs even when rescue fails. You can nest blocks inside rescue for multi-level error handling.
How do I re-raise an error after rescue?
Use ansible.builtin.fail at the end of your rescue section: fail: msg="Recovered but flagging failure". This marks the host as failed after your cleanup completes.
Conclusion
Ansible's block/rescue/always is essential for production playbooks:
• block — Group tasks and apply shared directives
• rescue — Run recovery steps when block fails
• always — Guaranteed cleanup (temp files, services, locks)
• Use ansible_failed_task/ansible_failed_result — Get error context in rescue
Every playbook that modifies production systems should use block/rescue/always for safe rollbacks and guaranteed cleanup.
Related Articles
• Ansible Ignore Errors Complete Guide • Ansible Error Handling: failed_when & changed_when Guide • Ansible Check Mode (Dry Run) Complete Guide • Ansible Handlers: Trigger Actions on ChangeCategory: installation