Ansible Error Handling: block, rescue, and always Explained (2026 Guide)
Objective
This guide covers structured error handling in Ansible playbooks using block, rescue, and always. By the end, you will know how to:
- Group related tasks using
blockso they can be managed as a single unit - Catch and handle task failures gracefully with the
rescuesection - Execute cleanup and logging tasks regardless of success or failure using
always - Build playbooks that recover automatically from expected failures instead of stopping completely
- Apply these patterns to real production automation scenarios
Ansible playbooks fail. That is not a bug, it is a reality of automating real infrastructure where services are sometimes down, packages are sometimes unavailable, and network conditions are sometimes unreliable. The question is whether your playbook fails gracefully with a sensible recovery action or fails badly and leaves the system in an unknown state. block, rescue, and always are how you make the answer consistently the former.
The Problem: Default Ansible Failure Behaviour
By default, when a task fails in an Ansible playbook, execution stops for that host. The remaining tasks in the play are skipped. If the failed task was part of a multi-step operation such as installing a service, starting it, and verifying it, you may end up with a partially configured system and no automatic recovery.
For simple one-off tasks that is acceptable. For production automation that runs on critical infrastructure, it is not. You need a way to define what should happen when something goes wrong, not just what should happen when everything goes right.
The Solution: block, rescue, and always
Think of this structure the same way you think of try/catch/finally in a programming language. The mapping is direct:
Basic Structure
Here is the skeleton of a playbook using all three sections:
---
- name: Demonstrate block, rescue, and always
hosts: all
tasks:
- name: Error handling example
block:
- name: Task 1 - attempt the main work
ansible.builtin.command: /usr/local/bin/deploy.sh
- name: Task 2 - additional step if task 1 succeeds
ansible.builtin.service:
name: myapp
state: started
rescue:
- name: Recovery task - runs only if block failed
ansible.builtin.debug:
msg: "Deployment failed. Running rollback."
- name: Rollback the deployment
ansible.builtin.command: /usr/local/bin/rollback.sh
always:
- name: Cleanup task - runs regardless of outcome
ansible.builtin.debug:
msg: "Deployment attempt finished. Logging result."
The flow in plain language:
- Ansible runs the tasks in
blockin order - If any task in
blockfails, Ansible skips the remainingblocktasks and runsrescueinstead - After either
blockorrescuecompletes,alwaysruns no matter what happened
Practical Examples
Example 1: Service Deployment with Rollback
This is the most common real-world pattern. Install a package, configure it, start the service. If anything fails, roll back and send a notification. Always log the outcome.
---
- name: Deploy web application
hosts: webservers
tasks:
- name: Web application deployment
block:
- name: Install the application package
ansible.builtin.dnf:
name: myapp
state: present
- name: Deploy configuration file
ansible.builtin.template:
src: myapp.conf.j2
dest: /etc/myapp/myapp.conf
owner: root
group: root
mode: '0644'
- name: Start and enable the application service
ansible.builtin.service:
name: myapp
state: started
enabled: true
- name: Verify the application is responding
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
rescue:
- name: Log the failure
ansible.builtin.debug:
msg: "Deployment failed on {{ inventory_hostname }}. Starting rollback."
- name: Remove the failed package
ansible.builtin.dnf:
name: myapp
state: absent
- name: Notify the team of the failure
ansible.builtin.mail:
to: ops-team@example.com
subject: "Deployment failed on {{ inventory_hostname }}"
body: "The myapp deployment failed. Rollback has been initiated."
always:
- name: Record deployment attempt in the log
ansible.builtin.lineinfile:
path: /var/log/deployments.log
line: "{{ ansible_date_time.iso8601 }} - Deployment attempted on {{ inventory_hostname }}"
create: true
Example 2: Database Backup with Guaranteed Cleanup
Run a backup operation. If it fails, flag the failure and clean up any partial files. Always release the lock file regardless of what happened, because a stuck lock file that prevents future backups is a common production problem.
---
- name: Database backup with error handling
hosts: dbservers
vars:
backup_dir: /var/backups/db
lock_file: /var/run/db_backup.lock
tasks:
- name: Database backup process
block:
- name: Create backup lock file
ansible.builtin.file:
path: "{{ lock_file }}"
state: touch
- name: Run database dump
ansible.builtin.command:
cmd: "mysqldump --all-databases > {{ backup_dir }}/backup_{{ ansible_date_time.date }}.sql"
- name: Compress the backup file
ansible.builtin.archive:
path: "{{ backup_dir }}/backup_{{ ansible_date_time.date }}.sql"
dest: "{{ backup_dir }}/backup_{{ ansible_date_time.date }}.sql.gz"
remove: true
rescue:
- name: Log the backup failure
ansible.builtin.debug:
msg: "Backup failed on {{ inventory_hostname }}. Cleaning up partial files."
- name: Remove any partial backup files
ansible.builtin.find:
paths: "{{ backup_dir }}"
patterns: "backup_{{ ansible_date_time.date }}*"
register: partial_files
- name: Delete partial backup files
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ partial_files.files }}"
always:
- name: Remove the lock file regardless of outcome
ansible.builtin.file:
path: "{{ lock_file }}"
state: absent
Example 3: Checking ansible_failed_task and ansible_failed_result
Inside a rescue block, Ansible provides special variables that tell you exactly what failed and why. This is useful for building informative notifications and logs.
---
- name: Error handling with failure details
hosts: all
tasks:
- name: Task group with detailed error capture
block:
- name: Attempt a potentially failing task
ansible.builtin.command: /usr/local/bin/validate.sh
register: validation_result
rescue:
- name: Display what failed and why
ansible.builtin.debug:
msg:
- "Failed task: {{ ansible_failed_task.name }}"
- "Return code: {{ ansible_failed_result.rc | default('N/A') }}"
- "Error output: {{ ansible_failed_result.stderr | default('None') }}"
- name: Send detailed failure notification
ansible.builtin.uri:
url: https://alerts.example.com/webhook
method: POST
body_format: json
body:
host: "{{ inventory_hostname }}"
task: "{{ ansible_failed_task.name }}"
error: "{{ ansible_failed_result.stderr | default('Unknown error') }}"
always:
- name: Final status log
ansible.builtin.debug:
msg: "Block execution complete on {{ inventory_hostname }}"
Behaviour Details Worth Understanding
One important point from the second row: if the rescue section completes successfully, Ansible considers the overall task group to have succeeded, even though the block failed. This is by design: rescue is a legitimate recovery path, not just error logging.
Common Patterns and When to Use Each
Combining with ignore_errors and failed_when
Block and rescue are not the only error handling tools in Ansible. They work best alongside two other directives:
# ignore_errors: continue even if this specific task fails
# Useful when a task failing is expected and acceptable
- name: Stop the service (may already be stopped)
ansible.builtin.service:
name: myapp
state: stopped
ignore_errors: true
# failed_when: define your own failure condition
# Useful when a command returns 0 but the output signals a problem
- name: Check application health
ansible.builtin.command: /usr/local/bin/healthcheck.sh
register: health_result
failed_when: "'ERROR' in health_result.stdout"
# Combining with block: use ignore_errors inside block for non-critical steps
# but let block/rescue handle critical failures
- name: Deployment with mixed error handling
block:
- name: Optional pre-flight check
ansible.builtin.command: /usr/local/bin/preflight.sh
ignore_errors: true
- name: Critical deployment step
ansible.builtin.command: /usr/local/bin/deploy.sh
rescue:
- name: Handle critical deployment failure
ansible.builtin.debug:
msg: "Critical step failed, running recovery"
Common Mistakes
- Putting rescue tasks that can also fail without their own error handling. If a task in
rescuefails, the entire block fails with no further recovery. For critical rescue operations, consider nesting a block inside rescue.- Keep rescue tasks simple and reliable. A rollback script that can also fail is not a safe rescue action.
- Forgetting that rescue changes the final status. A successful rescue means the play host is marked as succeeded. If you need to mark the play as failed even after a successful rescue, use
ansible.builtin.failat the end of your rescue block. - Treating always as a post-task handler.
alwaysruns for that specific block, not at the end of the entire play. For play-level cleanup, use Ansible handlers or a separate task section after the block. - Not logging enough detail in rescue. When an automated recovery runs, you need to know it happened. Always include a
debugor logging task at the start of rescue so the failure is visible in the playbook output.- Use
ansible_failed_task.nameandansible_failed_resultto capture exactly what went wrong
- Use
- Making blocks too large. A block with 15 tasks where any one failure triggers rescue makes it difficult to diagnose which task actually failed. Keep blocks focused on a single logical operation.
Quick Reference
# Full block/rescue/always structure
block:
- name: Main tasks here
...
rescue:
- name: Recovery tasks here (only on block failure)
...
# Special variables available in rescue:
# ansible_failed_task.name - name of the task that failed
# ansible_failed_result - full result object from the failed task
# ansible_failed_result.rc - return code
# ansible_failed_result.stderr - error output
always:
- name: Cleanup tasks here (always runs)
...
Error Handling and the RHCE Exam
Structured error handling with block, rescue, and always is an explicit RHCE exam objective. The exam tests your ability to write playbooks that handle failures gracefully rather than just stopping, which means you need to be able to:
- Write a
blocksection grouping related tasks - Add a
rescuesection that executes meaningful recovery actions when the block fails - Add an
alwayssection for tasks that must run regardless of outcome - Use the special
ansible_failed_taskandansible_failed_resultvariables in rescue sections - Understand how the final host status is determined when rescue succeeds vs fails
The pattern appears in real exam tasks framed as: "configure the playbook so that if the deployment fails, the system is rolled back to its previous state, and the result is logged regardless of outcome." That description maps directly to block/rescue/always.
Practice This in a Real Environment
The best way to understand error handling is to break things on purpose. Write a block task that you know will fail, watch rescue trigger, confirm always runs, then check what ansible_failed_result contains. None of that intuition comes from reading the playbook structure on a page.
LinuxCert.Guru has a dedicated hands-on lab for this: Error Handling in Ansible. It covers block, rescue, and always through real exercises that require you to implement fault-tolerant playbooks, with auto-graded tasks that verify your error handling actually works as intended.
Practice the Ansible Error Handling lab at LinuxCert.Guru → https://linuxcert.guru/rhce/?name=rhce-error-handling
Conclusion
Ansible's block, rescue, and always sections give you the same structured error handling that good programming practice demands, applied directly to infrastructure automation. The pattern is clean, the behaviour is predictable, and the alternative, playbooks that stop completely on any unexpected failure, is not acceptable for production environments.
blockgroups the tasks you want to run,rescuedefines what happens when they don't,alwayshandles what must happen regardless- A successful rescue marks the host as succeeded, even if the block failed. Use
ansible.builtin.failat the end of rescue if you need to preserve the failure status. ansible_failed_taskandansible_failed_resultgive you exact details about what went wrong inside rescue- Keep rescue tasks simple and reliable. A rescue that can also fail without its own recovery is a problem waiting to happen.
- Always run blocks on a real system and test the failure path deliberately. Knowing the theory is not the same as having watched rescue trigger and confirmed always ran.
Start the Ansible Error Handling lab at LinuxCert.Guru → https://linuxcert.guru/rhce/?name=rhce-error-handling