AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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

VariableDescription
ansible_failed_taskThe task object that failed (name, action, etc.)
ansible_failed_resultThe result dict from the failed task (msg, rc, stderr, etc.)

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"

See also: Ansible troubleshooting - Error internal-error

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

Featureblock/rescueignore_errors: true
Error handlingCustom recovery logicSilently continues
Rollback✅ Yes❌ Manual only
Error infoansible_failed_task, ansible_failed_resultOnly via register
Cleanupalways sectionManual
Play statusRescued = successIgnored = success
Use whenRecovery steps neededError is acceptable

Best Practices

  1. Always use always for cleanup — temp files, locks, service states
  2. Don't overuse rescue — Only for real recovery logic, not suppressing errors
  3. Log failures in rescue — Don't silently swallow errors
  4. Test rescue paths — Force failures in staging to verify rollback works
  5. Use ansible_failed_task — Log which specific task failed for debugging
  6. 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.

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home