// learn Β· linux Β· rhce Β· 9 min

Ansible error handling.

try/catch/finally for your plays. A failed task usually halts the host β€” but `block`/`rescue`/`always`, `failed_when`, `ignore_errors`, and `assert`/`fail` let you decide exactly when a play recovers, when it cleans up, and when it should stop loudly.

Ansible error handling animated tutorial. Default failure behavior where a failed task halts the affected host while others continue, any_errors_fatal and max_fail_percentage to tune the blast radius, ignore_errors and failed_when to redefine failure, changed_when, block rescue always as try catch finally with ansible_failed_task and ansible_failed_result, assert and fail to guard preconditions, meta end_play, and --force-handlers. RHCE EX294 ready.
// ansible Β· error handling

A failed task usually halts the host β€” but you decide how brittle a play really is. Watch a deploy fail, jump to rescue, recover, and still run its cleanup, then see what changes when the block succeeds.

// before you start
you should know
  • Β·You can write and run an Ansible playbook
  • Β·Comfortable with tasks, register, and when:
  • Β·Know what a non-zero exit code means
by the end you'll

Control failure in a play: tune the blast radius, redefine what counts as failed, recover with block/rescue/always, and stop deliberately with assert and fail.

pace: 9 minutes

ansible β€” step 1 / 5 Β· default
default: failed task halts the host
block:run these
install packageβœ“ ok
template configβœ— failed
start serviceβ€” skipped
β”‚ on failure ↓ β”‚
rescue:catch β€” recovery
(no rescue defined)β—‹ idle
β”‚ no matter what ↓ β”‚
always:finally β€” cleanup
(no always defined)β—‹ idle

What failure does by default

When a task fails on a host, Ansible STOPS running further tasks on that host β€” but keeps going on every other host in the play. The remaining tasks for the failed host are skipped and it drops out of the run. You tune this blast radius with `any_errors_fatal: true`, which aborts the whole play the instant any host fails, or with `max_fail_percentage`, which lets you tolerate a fraction of the fleet failing before bailing out.

$- name: template config ansible.builtin.template: ... # β†’ FAILED
$any_errors_fatal: true
$max_fail_percentage: 30
// key insight

By default a failed task halts that host. block/rescue/always give you try/catch/finally β€” recover in rescue, clean up in always β€” while failed_when/ignore_errors let you decide what even counts as failure. Together they turn a brittle playbook into one that fails loudly only when it should.

// exam-ready Β· error handling & recovery
$(default)
$any_errors_fatal: true
$max_fail_percentage: N
$ignore_errors: true
$failed_when: COND
$changed_when: COND / false
$block: / rescue: / always:
$ansible_failed_task
$ansible_failed_result
$assert: that: [..] fail_msg/success_msg
$fail: msg="..." (+ when:)
$meta: end_play / end_host
$--force-handlers
$register: result
// check yourself
4 quick questions
Q1

By default, a task fails on one of five hosts. What happens?

Q2

When do `rescue:` tasks run?

Q3

When do `always:` tasks run?

Q4

A `command` task exits non-zero, but for your case that's actually fine. How do you stop Ansible treating it as a failure?

These aren't graded β€” they're just for active recall, which is what actually makes the lesson stick.

πŸ“¦
// keep going

Ansible roles

Once a play is robust, make it reusable. Roles structure your tasks, handlers, vars, and templates into a tidy, shareable package you can drop into any playbook.

open β†’

Read this lesson as text5 chapters Β· quiz answers

  1. 01What failure does by default

    When a task fails on a host, Ansible STOPS running further tasks on that host β€” but keeps going on every other host in the play. The remaining tasks for the failed host are skipped and it drops out of the run. You tune this blast radius with any_errors_fatal: true, which aborts the whole play the instant any host fails, or with max_fail_percentage, which lets you tolerate a fraction of the fleet failing before bailing out.

    • $ - name: template config ansible.builtin.template: ... # β†’ FAILED β€” later tasks on this host skip
    • $ any_errors_fatal: true β€” any host fails β†’ abort whole play
    • $ max_fail_percentage: 30 β€” tolerate up to 30% failing
  2. 02ignore_errors & failed_when

    Sometimes a non-zero exit isn't really a failure for your case. ignore_errors: true lets the play march on past a failed task instead of halting the host. failed_when: redefines WHAT failure even means β€” fail only when stderr contains ERROR, say. changed_when: does the same for the changed status, so a read-only command can report changed_when: false. Register the result first, then express the condition against it.

    • $ ignore_errors: true β€” continue even if this task fails
    • $ failed_when: "'ERROR' in result.stderr" β€” only THIS counts as failure
    • $ register: result failed_when: result.rc not in [0, 2] β€” rc 2 is fine here
  3. 03block / rescue / always

    Group related tasks under a block:. If ANY task in the block fails, Ansible jumps to the rescue: tasks β€” your recovery path, like a catch. always: runs no matter what happened, success or failure, like a finally clause β€” perfect for releasing locks or tearing down temp state. This is Ansible's structured error handling, and rescue blocks can inspect ansible_failed_task and ansible_failed_result to react to exactly what broke.

    • $ block: - ...risky tasks rescue: - ...recovery always: - ...cleanup β€” the whole structure
    • $ debug: var=ansible_failed_task.name β€” what failed, inside rescue
    • $ always: - file: path=/run/app.lock state=absent β€” cleanup runs regardless
  4. 04assert & fail β€” stop on purpose

    Sometimes you WANT to stop. assert: checks the conditions in its that: list and fails with a custom fail_msg if any are false β€” ideal for validating preconditions before you touch anything. fail: stops immediately with your message, usually guarded by a when: so it only fires in the bad case. For finer control, meta: end_play ends the play for everyone and end_host bows out just the current host.

    • $ assert: that: - ansible_distribution == 'RedHat' fail_msg: "RHEL only" β€” validate a precondition
    • $ fail: msg: "unsupported OS" when: ansible_os_family != 'RedHat' β€” bail out on purpose
    • $ meta: end_play β€” end play (or end_host)
  5. 05Putting it together β€” robust plays

    Stack the tools. Put assert at the top to validate inputs before anything runs, wrap the risky steps in block/rescue so a failure becomes a controlled recovery, and drop teardown in always so locks and temp files always get cleaned up. Reserve ignore_errors for genuinely optional steps. Pair the whole thing with --force-handlers so a mid-play failure still flushes the handlers a host already notified.

    • $ assert: that: [ required_var is defined ] β€” fail fast on bad input
    • $ block: [risky] rescue: [notify ops] always: [cleanup] β€” controlled recovery + teardown
    • $ ansible-playbook site.yml --force-handlers β€” flush handlers despite a failure

Quiz β€” answers

By default, a task fails on one of five hosts. What happens?
β†’That host stops running further tasks; the other four keep going β€” unless any_errors_fatal or max_fail_percentage say otherwise.A failed task halts only the affected host; its remaining tasks are skipped and it drops out of the run, while the rest of the fleet continues. You change that with any_errors_fatal: true (abort everything) or max_fail_percentage (tolerate a fraction).
When do rescue: tasks run?
β†’Only when a task in the associated block: failed.rescue is the catch clause: it runs only if a task inside its block failed. If the block succeeds, rescue is skipped entirely.
When do always: tasks run?
β†’Always β€” whether the block succeeded or failed β€” like a finally clause.always is the finally clause: it runs regardless of whether the block succeeded or failed (and regardless of whether rescue ran). It's where cleanup like releasing locks belongs.
A command task exits non-zero, but for your case that's actually fine. How do you stop Ansible treating it as a failure?
β†’Set failed_when to redefine the failure condition (e.g. failed_when: result.rc not in [0,2]) β€” or ignore_errors: true to simply continue past it.failed_when redefines what counts as failure based on the registered result, so the task is marked ok. ignore_errors: true is the blunter option β€” the task still shows as failed, but the play continues anyway.
// more in systems

keep going β€” these pair well with what you just learned.

see all systems β†’

See also

back to RHCSA / RHCE trackall lessons