unit 5 · Logic

Blocks & error handling

try/catch/finally you already know

essential · 7 min · linux + windows

you may want to read 15 · Registering output & debug , 18 · Conditionals with when first — but nothing is locked

tl;dr

  • A failed task drops that host from the play — every other host carries on. Containment is per host.
  • failed_when redefines failure from the result; ignore_errors just barrels past a real one.
  • block groups tasks; rescue runs on failure, always runs regardless — try/catch/finally.

why it matters Production playbooks need rollback paths and checks that may "fail" — without failed_when and rescue you either bury real errors or let one bad exit code strand a host mid-deploy.

Lesson 15 ended with a warning shot: register nginx -t, and if the config is broken the task fails and the play stops before your debug ever runs. This lesson fixes it. First, be precise about what “stops” means. When a task fails, Ansible pulls that host out of the rest of the play — every other host carries on. One broken box in a twenty-host run kills the run only for that box, and the recap counts it under failed. That per-host containment is the default, and most days it’s exactly what you want. Everything below is for the days it isn’t.

Two dials on a single task

The blunt dial is ignore_errors: true. The task still runs, still fails, still prints its red error with ...ignoring appended — but the host stays in the play, and the recap files it under ignored, not failed. Use it for genuinely optional work, and sparingly: it doesn’t make a task succeed, it just declares that nobody cares.

The precise dial is failed_when: — it redefines what failure means for one task. Like when: from lesson 18 it takes a bare Jinja2 expression, no braces. It’s evaluated against the task’s own result, so it pairs naturally with register . Here’s lesson 15’s trap, defused:

- name: Check nginx config syntax
  ansible.builtin.command: nginx -t
  register: cfg_check
  failed_when: false # a nonzero rc no longer fails the host

- name: Reload nginx only if the config parses
  ansible.builtin.service:
    name: nginx
    state: reloaded
  when: cfg_check.rc == 0 # the registered rc drives the decision
failed_when: false — capture the verdict, decide later

failed_when: false means the check can never fail the host. The exit code lands safely in cfg_check, and a later task decides what to do about it. The other everyday shape is rc-based. Plenty of commands use nonzero exit codes as answers, not errors — grep returns 1 for “no match” — so failed_when: grep_result.rc not in [0, 1] fails only on a genuine error code. Its sibling changed_when: redefines the changed verdict the same way. That dial is the heart of lesson 22.

block, rescue, always

A block groups tasks. Any keyword you put on the block — a when:, a become: — applies to every task inside. That is the “one condition on a whole group of tasks” lesson 18 promised. But blocks live in this lesson for the two sections you can attach. rescue: runs only if a task in the block fails. always: runs no matter what happened.

- name: Deploy with a rollback path
  block:
    - name: Run the database migration
      ansible.builtin.command: /opt/app/bin/migrate.sh
  rescue:
    - name: Roll back to the previous release
      ansible.builtin.command: /opt/app/bin/rollback.sh
  always:
    - name: Re-enable monitoring alerts
      ansible.builtin.command: /opt/app/bin/alerts.sh enable
a deploy that can undo itself

Block tasks run in order. The first failure jumps straight to rescue:, skipping the rest of the block on that host. If the rescue tasks succeed, the host is healthy again — it continues with the rest of the play, and the recap shows rescued=1 instead of a failure. Inside a rescue, two variables tell you what broke: ansible_failed_task and ansible_failed_result hold the failing task and its result dict, ready for lesson 15’s debug treatment.

One habit ties this together: reach for failed_when: before ignore_errors:, and for rescue: when the response to failure is real work — rollback, cleanup, re-arming alerts — rather than a shrug. Your recap vocabulary is now complete: ok, changed and skipping from earlier lessons (the recap column spells it skipped), joined by failed, unreachable, rescued and ignored. Next lesson takes the one verdict that still lies — command reporting changed for work it never did — and makes it tell the truth.

self-check — recall, not a test

  1. 1 One host of twenty fails a task mid-play, no error handling anywhere. What happens to that host, and to the other nineteen?

  2. 2 ignore_errors and failed_when both keep a run alive — when is each the right tool?

  3. 3 Map block/rescue/always to PowerShell, and name one failure rescue will not catch.

lab (optional — never required)

Against localhost: put a failing command in a block, debug ansible_failed_result in rescue, add an always task — then watch the recap report rescued=1 and the play finish clean.

needs: The control node from lesson 5.

next up 22 · Idempotency, deeply 8 min · unit 6