Skip to content
Linux Basics: Check Background Tasks with Bash wait

Linux Basics: Check Background Tasks with Bash wait

October 21, 2026

Starting a command in the background only tells you that it was launched. To learn whether its work succeeded, keep its process ID and wait for its completion. Bash’s wait builtin lets a script collect those results without guessing from a delay or a process listing.

These examples were tested on Linux with GNU Bash 5.2.37. Use Bash rather than sh. They start short-lived child processes with deliberate exit statuses. They do not stop existing processes, change services, or launch persistent jobs.

Prepare a scratch directory

You need Bash, a text editor, and the sleep, mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file in this directory before running it. The guards stop the shell if setup fails; invoking bash explicitly avoids changing executable permissions. No administrator access is needed.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Save the child ID and collect its result

Save this as wait-one.sh:

#!/usr/bin/env bash
(sleep 0.05; exit 7) &
child=$!
printf 'Background task started\n'
if wait "$child"; then
  status=0
else
  status=$?
fi
printf 'Completion status: %d\n' "$status"

Run it:

bash wait-one.sh

Output:

Background task started
Completion status: 7

The ampersand lets the parent continue while the subshell runs. $! identifies the most recently launched background task, so save it before starting another one. wait blocks if necessary, then returns that child’s status. It also works when the saved child has already completed.

The child exits with seven on purpose. The conditional handles that outcome, and the failure branch saves $? immediately. A later printf would replace it. The launch itself is not evidence of success, and sleeping for an estimated duration is not a substitute for collecting the result.

2. Check every task, even after one fails

Save this as wait-each.sh:

#!/usr/bin/env bash
pids=()
labels=(index archive)
for code in 0 5; do
  (exit "$code") &
  pids+=("$!")
done
failed=0
for index in "${!pids[@]}"; do
  if wait "${pids[index]}"; then
    status=0
  else
    status=$?
    failed=$((failed + 1))
  fi
  printf '%s status: %d\n' "${labels[index]}" "$status"
done
printf 'Failed tasks: %d\n' "$failed"
if (( failed > 0 )); then
  exit 1
fi

Run it:

bash wait-each.sh
printf 'Script status: %d\n' "$?"

Output:

index status: 0
archive status: 5
Failed tasks: 1
Script status: 1

Both children start before the waiting loop. The parent checks their saved IDs in launch order, which makes the displayed report deterministic even if they finish in a different order. Waiting for the first task does not pause a second task that is already running.

The loop collects every result rather than abandoning later tasks after the first failure. Its final status is one if any task failed. The displayed count and each task’s original status provide detail without trying to encode every failure in a single exit code. These children print nothing, so their output cannot interleave with the report.

3. Do not use a bare wait as an aggregate success test

Save this as wait-without-ids.sh:

#!/usr/bin/env bash
(exit 6) &
(exit 0) &
wait
status=$?
printf 'wait without IDs: %d\n' "$status"
printf 'All children finished, but individual results were not checked\n'

Run it:

bash wait-without-ids.sh

Output:

wait without IDs: 0
All children finished, but individual results were not checked

With no IDs, wait waits for outstanding background jobs and returns zero. In this example that includes a child that failed with status six. Zero therefore means the waiting operation completed, not that every job succeeded. Use the individual-ID pattern above when failures matter.

Passing several IDs to a single wait is also not an all-success check: it reports the status for the last specified ID. Save and inspect one result at a time if you need a complete report. Keep the launch and the waits in the same parent shell.

Keep in mind

wait is for the shell’s own children, not an arbitrary process ID copied from ps. It does not provide a timeout, retry policy, or cancellation policy by itself. For real concurrent jobs, also plan separate output handling and a sensible concurrency limit. If using set -e, keep expected nonzero waits in conditionals as shown so a handled task failure does not prematurely end result collection.

Clean up

In the same terminal, remove only the named practice files, return to your original directory, and remove the empty scratch directory:

rm -- wait-one.sh wait-each.sh wait-without-ids.sh
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

References

Acknowledgements

This post was created with assistance from AI.

Last updated on