Implementing Error Handling in Ansible with Block, Rescue, and Always
Learn how to use Ansible block, rescue, and always constructs to handle task failures, perform rollbacks, and ensure system cleanup.
27 Jul 2026, 07:30 UTC

When an Ansible task fails, the default behavior is to stop execution for that specific host immediately. This often leaves systems in an inconsistent state—such as a service being stopped but not replaced, or temporary files remaining on disk. To manage these failures gracefully, use the block construct to group tasks and define recovery paths.
The Mechanism of Error Handling
The block structure functions as a logical grouping that allows for exception-style handling. It consists of three optional sections:
- block: The primary set of tasks to execute. If any task here fails, Ansible stops the block and jumps directly to the rescue section.
- rescue: Tasks that run only if a task in the
blockfails. This is used for rollbacks or alerting. - always: Tasks that run regardless of whether the block succeeded or the rescue was triggered. This is used for mandatory cleanup.
Example: Service Deployment with Rollback
In this scenario, we attempt to update a configuration file and restart a service. If the restart fails, the rescue block restores the previous configuration to ensure the service remains available in its last known good state.
- name: Application Update with Recovery
hosts: app_servers
become: yes
tasks:
- block:
- name: Backup current configuration
copy:
src: /etc/myapp/config.conf
dest: /etc/myapp/config.conf.bak
remote_src: yes
- name: Deploy new configuration
template:
src: config.conf.j2
dest: /etc/myapp/config.conf
- name: Restart application service
systemd:
name: myapp
state: restarted
rescue:
- name: Restore backup configuration
copy:
src: /etc/myapp/config.conf.bak
dest: /etc/myapp/config.conf
remote_src: yes
- name: Restart service after rollback
systemd:
name: myapp
state: restarted
always:
- name: Remove temporary build artifacts
file:
path: /tmp/myapp_build_dir
state: absent
Critical Limits and Behaviors
To avoid unexpected play results, keep these operational constraints in mind:
- Failure Status: A successful
rescueblock does not automatically mark the play as successful. The host will still be flagged as failed at the end of the play. To override this and force a "successful" status after a rescue, you must applyignore_errors: trueto theblock. - Unreachable Hosts: The
rescuesection only triggers on task failures (e.g., a command returning a non-zero exit code). It does not catch "unreachable" errors, such as SSH connection failures, as the host is no longer considered active for task execution. - Variable Scoping: Variables registered within a
rescueblock are not automatically persisted as global facts. If you need to use a value from the rescue section in a later play, use theset_factmodule.
Common Implementation Pitfalls
Incorrect Conditional Placement
Applying a when statement to a block affects the entire group. If the condition is false, the block is skipped entirely. Because no failure occurred during the execution of the block (since it was never entered), the rescue section will not run. If you need conditional recovery, place the when clause on individual tasks inside the block.
Silent Failures via ignore_errors
If you place ignore_errors: true on a specific task inside the block, that task's failure will not trigger the rescue section. Ansible treats the error as handled, and execution continues to the next task in the block.
Verification
To verify your error handling logic, run a test playbook using a command that intentionally fails, such as command: /bin/false. Run the playbook and confirm that:
- The
rescuetasks are executed. - The
alwaystasks are executed. - The final play recap shows the host as "failed" (unless
ignore_errors: truewas applied to the block).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.