Introduction
Ansible's block/rescue/always is the equivalent of try/catch/finally in programming languages. block groups tasks that might fail, rescue handles failures, and always runs cleanup regardless of success or failure. This pattern replaces fragile ignore_errors with structured error handling.
Basic Structure
- name: Deploy with error handling
block:
# TRY — tasks that might fail
- name: Deploy new version
ansible.builtin.unarchive:
src: "app-{{ version }}.tar.gz"
dest: /opt/app/
- name: Run database migration
ansible.builtin.command: /opt/app/migrate.sh
- name: Restart application
ansible.builtin.service:
name: myapp
state: restarted
rescue:
# CATCH — runs only if block fails
- name: Rollback to previous version
ansible.builtin.command: /opt/app/rollback.sh
- name: Notify team of failure
ansible.builtin.uri:
url: "{{ slack_webhook }}"
body: '{"text": "❌ Deploy failed on {{ inventory_hostname }}, rolled back"}'
always:
# FINALLY — runs regardless of success or failure
- name: Remove temporary files
ansible.builtin.file:
path: /tmp/deploy-staging/
state: absent
- name: Re-enable monitoring
ansible.builtin.uri:
url: "{{ monitoring_api }}/enable/{{ inventory_hostname }}"
block — Group Tasks
# Apply shared attributes to multiple tasks
- name: Install and configure app
block:
- ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: [nginx, python3, redis]
- ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/config.yml
- ansible.builtin.service:
name: myapp
state: started
become: true
when: ansible_os_family == "Debian"
# become and when apply to ALL tasks in the block
rescue — Handle Failures
- block:
- name: Pull Docker image
community.docker.docker_image:
name: "myapp:{{ new_version }}"
source: pull
- name: Start new container
community.docker.docker_container:
name: myapp
image: "myapp:{{ new_version }}"
state: started
rescue:
- name: Log the error
ansible.builtin.debug:
msg: "Deploy failed: {{ ansible_failed_result }}"
- name: Start previous version
community.docker.docker_container:
name: myapp
image: "myapp:{{ current_version }}"
state: started
# After rescue completes successfully, play continues
# The host is NOT marked as failed
rescue Variables
rescue:
# These special variables are available in rescue
- ansible.builtin.debug:
msg:
- "Failed task: {{ ansible_failed_task.name }}"
- "Failed result: {{ ansible_failed_result }}"
always — Guaranteed Cleanup
# Maintenance window pattern
- block:
- name: Disable alerts
ansible.builtin.uri:
url: "{{ pagerduty_api }}/maintenance"
method: POST
- name: Perform upgrade
ansible.builtin.include_role:
name: upgrade
always:
# Re-enable alerts whether upgrade succeeded or failed
- name: Enable alerts
ansible.builtin.uri:
url: "{{ pagerduty_api }}/maintenance"
method: DELETE
# Lock file pattern
- block:
- name: Acquire lock
ansible.builtin.copy:
content: "{{ ansible_date_time.iso8601 }}"
dest: /tmp/deploy.lock
- name: Run deployment
ansible.builtin.include_tasks: deploy.yml
always:
- name: Release lock
ansible.builtin.file:
path: /tmp/deploy.lock
state: absent
Real-World Patterns
Rolling Deploy with Rollback
- hosts: webservers
serial: 1
tasks:
- block:
- name: Remove from LB
community.general.haproxy:
state: disabled
host: "{{ inventory_hostname }}"
delegate_to: "{{ lb_host }}"
- name: Deploy new version
ansible.builtin.include_role:
name: deploy
- name: Health check
ansible.builtin.uri:
url: "http://localhost:{{ app_port }}/health"
register: health
until: health.status == 200
retries: 10
delay: 5
rescue:
- name: Rollback
ansible.builtin.include_role:
name: rollback
- name: Fail the host
ansible.builtin.fail:
msg: "Deploy failed on {{ inventory_hostname }}, rolled back"
always:
- name: Re-add to LB
community.general.haproxy:
state: enabled
host: "{{ inventory_hostname }}"
delegate_to: "{{ lb_host }}"
Database Migration with Savepoint
- block:
- name: Create database backup
ansible.builtin.command: >
pg_dump {{ db_name }} -f /backup/pre-migration.sql
- name: Run migration
ansible.builtin.command: /opt/app/migrate.sh
register: migration
- name: Verify migration
ansible.builtin.command: /opt/app/verify-schema.sh
rescue:
- name: Restore database
ansible.builtin.command: >
psql {{ db_name }} < /backup/pre-migration.sql
- name: Report failure
ansible.builtin.fail:
msg: "Migration failed: {{ ansible_failed_result.msg | default('unknown') }}"
always:
- name: Clean up backup (keep last 5)
ansible.builtin.shell: >
ls -t /backup/pre-migration*.sql | tail -n +6 | xargs rm -f
block vs ignore_errors
# ❌ ignore_errors — hides ALL errors, no recovery logic
- name: Risky operation
ansible.builtin.command: /opt/risky.sh
ignore_errors: true
# What went wrong? No idea. No recovery. No cleanup.
# ✅ block/rescue — structured error handling
- block:
- name: Risky operation
ansible.builtin.command: /opt/risky.sh
rescue:
- name: Handle the error specifically
ansible.builtin.debug:
msg: "Failed: {{ ansible_failed_result.stderr }}"
- name: Run recovery
ansible.builtin.command: /opt/recover.sh
Common Mistakes
# ❌ Rescue swallows the error silently
rescue:
- debug:
msg: "Something failed"
# Play continues as if nothing happened!
# ✅ Re-raise if you want the play to fail after cleanup
rescue:
- name: Clean up
ansible.builtin.command: /opt/cleanup.sh
- name: Fail with context
ansible.builtin.fail:
msg: "Deploy failed after cleanup: {{ ansible_failed_task.name }}"
# ❌ Nesting blocks too deeply
block:
- block:
- block: # 3 levels deep — unreadable!
# ✅ Extract to include_tasks or roles
block:
- ansible.builtin.include_role:
name: deploy
rescue:
- ansible.builtin.include_role:
name: rollback
Related Articles
- Ansible Error Handling
- Ansible changed_when / failed_when
- Ansible Troubleshooting
- Ansible Handlers Guide
Conclusion
block groups related tasks and applies shared attributes. rescue catches failures and runs recovery logic (rollback, notification). always guarantees cleanup runs regardless of outcome (release locks, re-enable monitoring, restore LB). This pattern replaces ignore_errors with intentional, structured error handling. The most common real-world use: deploy in block, rollback in rescue, re-enable load balancer in always.