⚠️ Exit Is Not as Strong as You Think when it comes to stopping ⚠️

You might think that exit 1 will do a hard stop on your script.

And while it will be a hard stop on the particular process that you are running in, that does not mean it will halt your script.

An example to illustrate this is as follows:

foo() {
  echo "foo: I am foo and I am exiting!!! [\$\$=$$/$BASHPID]"
  exit 1
}
export -f foo

main() {
  echo "MAIN: [\$\$=$$/$BASHPID]"

  local foo_result
  foo_result="$(foo)"

  echo "MAIN: And I don't care that you exited I am going to keep going"
  echo "MAIN: You said: [${foo_result:?}], but I am 'main' and had other plans."
  return 0
}

main "${@}" || exit 1

The output is as follows, where exit 1 did not stop main from continuing.

MAIN: [$$=3006467/3006467]
MAIN: And I don't care that you exited I am going to keep going
MAIN: You said: [foo: I am foo and I am exiting!!! [$$=3006467/3006472]], but I am 'main' and had other plans.

The reason is that $() spawned a new process (see Parenthesis Spawn a Subshell/New Process: $() AND ()). You can see this by the different $BASHPID that correctly identified the process id. So exit 1 only exited 3006472 PID and not 3006467.

In a subshell created by $(), exit 1 behaves similarly to return 1 in that it doesn’t propagate beyond that subshell.

Example how main should have collaborated, if we are using exit 1/return 1

foo() {
  echo "foo: I am foo and I am exiting!!! [\$\$=$$/$BASHPID]"
  exit 1
}
export -f foo

main() {
  echo "MAIN: [\$\$=$$/$BASHPID]"

  local foo_result
  foo_result="$(foo)" || {
    echo "MAIN: I will respect your wishes to exit..."
    exit 1
  }

  echo "Continuing... with happy foo result: ${foo_result:?}"
  return 0
}

main "${@}" || exit 1

PS

⚠️ One might think 'set -e' would save us in the first example but it does not ⚠️

set -e does not save us

If we set -e (See Flag for Exiting on non-zero (set -e)).

We could expect the first example to halt, due to non zero code returned by $(foo)

set -e

foo() {
  echo "foo: I am foo and I am exiting!!! [\$\$=$$/$BASHPID]"
  exit 1
}
export -f foo

main() {
  echo "MAIN: [\$\$=$$/$BASHPID]"

  local foo_result
  foo_result="$(foo)"

  echo "MAIN: And I don't care that you exited I am going to keep going"
  echo "MAIN: You said: [${foo_result:?}], but I am 'main' and had other plans."
  return 0
}

main "${@}" || exit 1

But the output will be still the same

MAIN: [$$=3012359/3012359]
MAIN: And I don't care that you exited I am going to keep going
MAIN: You said: [foo: I am foo and I am exiting!!! [$$=3012359/3012361]], but I am 'main' and had other plans.

The reason for that is the following line

main "${@}" || exit 1

And this behavior of set -e: ❌ Checking for failure/success, even up the chain prevents 'set -e' from triggering.❌