Handlers Patterns
Let’s see some of the real patterns that are used and solved with handlers. This combines everything we have been learning about handlers and other features of Ansible.
Pattern: Alerting From A rescue Block
rescue: isn’t limited to recovery logic — it can notify a completely different handler than the happy path would have, turning a failure into a genuine alert:
- name: Resilient Deployment With Alerting
hosts: servers
tasks:
- name: Attempt deployment
block:
- name: Deploy configuration
ansible.builtin.command: /bin/false
register: deploy_result
notify: Restart app
rescue:
- name: Notify the alert handler
ansible.builtin.debug:
msg: "Preparing alert"
changed_when: true
notify: Send alert
handlers:
- name: Restart app
ansible.builtin.debug:
msg: "Restarting the app"
- name: Send alert
ansible.builtin.debug:
msg: "ALERT: deployment failed on {{ inventory_hostname }}"ok: [ubuntu] => { "msg": "ALERT: deployment failed on ubuntu" }The deployment fails, so Restart app is never notified at all — a failed task doesn’t report changed, so Rule One from the handlers chapter keeps it from ever queuing. rescue: catches the failure and notifies Send alert instead — a genuinely different handler, reached only via the failure path.
force_handlers: Making Sure Pending Handlers Still Fire
Here’s a real, dangerous gap in the default behavior. If a handler is already queued, and a later, unrelated task in the same play then fails, the play stops — and the already-queued handler never runs at all, since handlers are deferred to the play’s natural end, which the play never actually reaches.
- name: Default Behavior (risky)
hosts: servers
tasks:
- name: Deploy configuration
ansible.builtin.copy:
src: app.conf
dest: /etc/app.conf
notify: Restart app
- name: A later, unrelated task that fails
ansible.builtin.command: /bin/false
handlers:
- name: Restart app
ansible.builtin.debug:
msg: "Restarting the app now"changed: [ubuntu]
fatal: [ubuntu]: FAILED! => {"msg": "non-zero return code"}Restart app never prints at all — the new config was deployed, but the play failed before ever reaching the point where queued handlers run, leaving a genuinely worse state than either fully succeeding or fully failing: a new config file sitting on disk, with the old process still running and never told to reload it.
force_handlers: true fixes this directly — queued handlers still run, even when a later task fails:
- name: Fixed With force_handlers
hosts: servers
force_handlers: true
tasks:
- name: Deploy configuration
ansible.builtin.copy:
src: app.conf
dest: /etc/app.conf
notify: Restart app
- name: A later, unrelated task that fails
ansible.builtin.command: /bin/false
handlers:
- name: Restart app
ansible.builtin.debug:
msg: "Restarting the app now"changed: [ubuntu]
ok: [ubuntu] => { "msg": "Restarting the app now" }
fatal: [ubuntu]: FAILED! => {"msg": "non-zero return code"}The play still ultimately fails — force_handlers doesn’t paper over the real failure — but Restart app runs regardless, keeping the config and the running process in sync even though something else genuinely went wrong.
Pattern: Deploy, Verify, Alert On Failure — All Together
Everything from this entire section, combined into one realistic deployment flow:
- name: Full Deployment Pattern
hosts: servers
force_handlers: true
tasks:
- name: Deployment attempt
block:
- name: Deploy configuration
ansible.builtin.copy:
src: app.conf
dest: /etc/app.conf
notify: Restart app
- name: Force restart to happen now
ansible.builtin.meta: flush_handlers
- name: Verify app responds after restart
ansible.builtin.command: /bin/false
register: verify_result
rescue:
- name: Alert on any failure in this deployment
ansible.builtin.debug:
msg: "Deployment or verification failed on {{ inventory_hostname }}"
changed_when: true
notify: Send alert
handlers:
- name: Restart app
ansible.builtin.debug:
msg: "Restarting the app"
- name: Send alert
ansible.builtin.debug:
msg: "ALERT: something failed on {{ inventory_hostname }}"Deploy, force the restart to actually happen before verification even starts, verify, and if anything at all in that sequence fails, rescue: catches it and alerts distinctly — with force_handlers standing guard in case some other, unrelated task elsewhere in the play fails too.
What You Just Did
block, rescue, notify, handler ordering, listen:, flush_handlers, and now force_handlers — six chapters’ worth of tools, all present in that one pattern above. And reaching further back still: inventory_hostname (the variables section’s magic variables), copy’s genuine idempotency (all the way back in the very first playbook chapter), and register/is changed (tests and conditionals) all show up too, doing exactly the jobs they were introduced to do. Nothing here is new mechanics — it’s composition, the same way the tests and conditionals section’s closing chapter was.
Best Practices
- Notify a dedicated alert handler from within
rescue:blocks, separate from your normal success-path handlers — a genuine failure deserves a genuinely different response, not silence or a generic restart. - Set
force_handlers: truefor any deployment-style playbook where a queued handler’s effect must happen regardless of what else in the play fails — leaving a change applied without its corresponding follow-up action is a worse outcome than either a clean success or a clean failure. - Combine
flush_handlers,rescue, andforce_handlersdeliberately when building genuinely resilient playbooks — each solves a different specific gap, and real reliability comes from knowing which one closes which gap, not from reaching for all of them out of habit.
That closes out this section. block and rescue for grouping and recovering from failure, handlers and their two core rules — including the loop-notification trip-up specifically flagged at the start of this section — execution ordering and listen:, and now force_handlers closing a real operational gap. The next section covers lookups: pulling data in from outside a playbook entirely, including the fileglob pattern this section’s with_X migration chapter already previewed.