Chapter 11Lesson 05~100 minutes

Designing Explicit Error Paths and Diagnostics

Strict shell options catch broad classes of mistakes. Production reliability comes from explicit error paths: clear classification, preserved status, actionable diagnostics, and a stable contract for callers.

IntermediateError handlingHands-on lab

Learning objectives

By the end of this lesson

  • Classify expected failure types.
  • Separate stdout results from stderr diagnostics.
  • Preserve exact failure status.
  • Return from reusable functions and exit at process boundaries.
  • Combine strict modes with explicit error policy.

1. Error handling is an interface

A production script should answer four questions for every important failure: what failed, why it failed, what the caller can do next, and what exit status represents the outcome.

Explicit error path
flowchart TD
  O["operation"] --> Q{"success?"}
  Q -->|"yes"| R["emit result"]
  Q -->|"no"| C["classify failure"]
  C --> D["diagnostic on stderr"]
  D --> E["cleanup / rollback"]
  E --> X["stable exit status"]

2. Distinguish expected domain failures from operational failures

ClassMeaningCaller action
Usage errorCaller supplied invalid argumentsCorrect invocation and retry
Policy rejectionInput is valid but not allowedChange environment/approval/policy
Dependency failureRequired tool/service unavailableRestore dependency
Operation failureRequested action failedInspect diagnostic and state

3. Diagnostics belong on stderr

printf 'artifact-id=%s\n' "$artifact_id"

printf 'error: upload failed endpoint=%s status=%d\n' \
  "$endpoint" "$status" >&2

Keeping stdout clean lets callers capture machine-readable results without mixing them with logs or errors.

4. Include enough context to identify the failing operation

printf 'error: deploy failed service=%q env=%q status=%d\n' \
  "$service" "$environment" "$status" >&2

Context should identify the resource and action, but avoid secrets, tokens, credentials, or unnecessary user data.

5. Keep exit codes small and stable

# Example contract:
# 0  success
# 64 invalid invocation
# 65 policy/domain rejection
# 69 dependency unavailable
# 70 internal/operational failure

Numeric statuses are for automation; the human-readable stderr message should explain the problem.

6. Centralize repeated diagnostic structure

die() {
  local status=$1
  shift
  printf 'error: %s\n' "$*" >&2
  exit "$status"
}

require_command() {
  command -v "$1" >/dev/null 2>&1 ||
    die 69 "required command not found: $1"
}
Do not hide control flow

A die helper is useful at top-level fatal boundaries. Deep library-style functions are often easier to reuse if they return and let the caller decide whether to exit.

7. Functions should return status; callers should decide policy

validate_service() {
  local service=$1
  [[ $service =~ ^[a-z0-9.-]+$ ]] || return 65
}

if validate_service "$service"; then
  :
else
  status=$?
  printf 'invalid service identifier: %q\n' "$service" >&2
  exit "$status"
fi

8. Save status before logging or cleanup

if external_tool "$input"; then
  :
else
  status=$?
  cleanup_partial_state
  printf 'external tool failed status=%d\n' "$status" >&2
  exit "$status"
fi

Running another command before saving $? destroys the original failure code.

9. Cleanup and rollback are different concepts

Cleanup removes temporary resources. Rollback attempts to restore externally visible state. Rollback may fail too, so diagnostics should preserve the primary failure while reporting rollback problems separately.

10. Strict modes support explicit error handling; they do not replace it

set -u
set -o pipefail
# set -e may also be used as a guardrail

main "$@"

Use strictness options to catch broad classes of bugs, then write explicit branches around operations where recovery, classification, or detailed diagnostics matter.

11. Put orchestration in main

main() {
  parse_args "$@" || return $?
  validate_config || return $?
  run_operation || return $?
}

if main "$@"; then
  exit 0
else
  status=$?
  printf 'operation failed status=%d\n' "$status" >&2
  exit "$status"
fi

A top-level main gives the script one clear place to convert internal statuses into process exit behavior.

12. Hands-on lab: explicit error-contract CLI

mkdir -p "$HOME/devops-academy/bash/chapter11/lesson05"
cd "$HOME/devops-academy/bash/chapter11/lesson05"

cat > release-check.sh <<'EOF'
#!/usr/bin/env bash
set -u
set -o pipefail

die() {
  local status=$1
  shift
  printf 'error: %s\n' "$*" >&2
  exit "$status"
}

service=${1:-}
environment=${2:-}

[[ -n $service && -n $environment ]] ||
  die 64 "usage: $0 SERVICE ENVIRONMENT"

case $environment in
  dev|staging|prod) ;;
  *) die 65 "unsupported environment: $environment" ;;
esac

command -v grep >/dev/null 2>&1 ||
  die 69 "required command missing: grep"

if printf '%s\n' "$service" |
   grep -Eq '^[a-z0-9][a-z0-9.-]*$'; then
  printf 'VALID service=%s environment=%s\n' \
    "$service" "$environment"
else
  status=$?
  die 65 "invalid service identifier (grep status=$status)"
fi
EOF

chmod u+x release-check.sh
./release-check.sh api staging
./release-check.sh 'API PROD' prod || true
./release-check.sh api qa || true

Verification checklist

13. Knowledge check

Question 1. Why keep stdout and stderr separate?

Question 2. When should a reusable function return instead of exit?

Question 3. What is the difference between cleanup and rollback?

Question 4. Do strict modes eliminate the need for explicit error branches?

14. Summary

Production error handling is explicit: classify failures, preserve exact status, emit actionable diagnostics on stderr, keep stdout clean, centralize process-level exit policy, and use strict modes as guardrails rather than substitutes for control flow.

15. Further reading

  • GNU Bash Reference Manual — Exit Status and Shell Builtins.
  • POSIX shell command language — exit status conventions.
  • Command Line Interface Guidelines (clig.dev) — errors and output streams.
  • ShellCheck documentation — error-path and quoting guidance.
Next lesson

Idempotency and Convergent Shell Operations

Chapter 12 will build on these error contracts with idempotency, retries, logging, locking, checkpoints, and production recovery patterns.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.