Consider the following code


set -e
source inner-scope.sh
echo $(inner)
echo "I thought I would've died :("


function inner() { echo "winner"; return 1; }

I'm trying to get outer-scope.sh to exit when a call to inner() fails. Since $() invokes a sub-shell, this doesn't happen.

How else do I get the output of a function while preserving the fact that the function may exit with a non-zero exit code?

3 Answers 3


$() preserves the exit status; you just have to use it in a statement that has no status of its own, such as an assignment.


After this, $? would contain the exit status of inner, and you can use all sorts of checks for it:

output=$(inner) || exit $?
echo $output


if ! output=$(inner); then
    exit $?
echo $output


if output=$(inner); then
    echo $output
    exit $?

(Note: A bare exit without arguments is equivalent to exit $? – that is, it exits with the last command's exit status. I used the second form only for clarity.)

Also, for the record: source is completely unrelated in this case. You can just define inner() in the outer-scope.sh file, with the same results.

  • Why is it that, even though $? contains the exit status of $() that the script does not exit automatically (given that -e is set)? EDIT: nevermind, I think you have answered my questions, thanks!
    – jabalsad
    Dec 2, 2011 at 16:12
  • I'm not sure. (I haven't tested any of the above.) But there are some restrictions on -e, all explained in bash's manpage; also, if you are asking about echo $(), then it might be because the subshells' exit codes are ignored when the line - the echo command - has an exit code (usually 0) of its own. Dec 2, 2011 at 18:10
  • 2
    Hmm, when I type if ! $(exit 1) ; then echo $?; fi, I get 0. Not sure if is the way to go if you need to preserve that exit value.
    – Ron Burk
    May 4, 2017 at 2:08
  • 6
    if ! output=$(inner); then exit $?; fi will exit with a return code of 0 because $? will give the return code of ! instead of the return code of inner. You could get the desired behavior with if output=$(inner); then : ; else exit $?; fi but that's obviously more verbose
    – SJL
    Apr 6, 2018 at 14:56
  • 5
    I unthinkingly assumed that since foo=$(...) was safe local foo=$(...) would be safe but it is not. Took me a long time before I even considered local might be the issue. Edit: I see that now mentioned in ryenus' answer Oct 10, 2021 at 20:47

According to the manual of the set builtin, the shell does not exit if:

  1. the command that fails is part of the command list immediately following a while or until keyword,
  2. part of the test in an if statement,
  3. part of any command executed in a && or || list except the command following the final && or ||,
  4. any command in a pipeline but the last,
  5. or if the command’s return status is being inverted with !.

Any of the above would work, except the first since you're not running a loop. Though for readability it's probably better to use an if statement:

set -e

if x=$(echo a; false); then
  echo "subshell worked"
  echo "subshell failed"

echo "x: $x"

Or you can invert the result:

bash -c 'set -e; ! x=$(echo a; false); echo "[$?]x:$x"'   # [0]x:a
bash -c 'set -e; ! y=$(echo b; true ); echo "[$?]y:$y"'   # [1]y:b

But beware that the result is now inverted. Fortunately bash doesn't exit even though a successful subshell is inverted.

Interestingly the following seems also work, by grouping the subshell invocation in a list, then invert the whole list, not just the subshell invocation:

bash -c 'set -e; ! { x=$(echo a; false); echo "[$?]x:$x"; }'  # [1]x:a

This somehow matches the 3rd case above, which is also a list, except it's not using && or ||.

A Special Case with Inline Initialization

Note about a tricky case with function local variables, compare the two almost identical functions below:

f() { local    v=$(echo data; false); echo output:$v, status:$?; }
g() { local v; v=$(echo data; false); echo output:$v, status:$?; }

In both functions, the subshell ends with a false command which causes it to fail, however, when executed, we'll get:

$ f     # fooled by 'local' with inline initialization
output:data, status:0

$ g     # good one, with separated declaration and initialization
output:data, status:1


In bash, local is actually a builtin command. When the output of a subshell is used to initialize a local variable, the exit status is no longer the one of the subshell, but that of the local command, which is 0 as long as the local variable gets declared.

See also https://stackoverflow.com/a/4421282/537554

  • 7
    While this didn't really answer the question, this came in useful to me today, so +1. Jul 27, 2016 at 1:32
  • 4
    The exit status for bash commands is always that of the last command executed. When we spend so much time in strongly typed languages it's easy to forget that "local" isn't a type specifier but just another command. Thanks for re-iterating this point here, helped me today. Sep 17, 2016 at 1:11
  • 4
    Wow, I ran into this exact issue just now and you cleared it up. Thanks!
    – krb686
    Jan 7, 2017 at 2:50
  • 1
    Just got bitten by the "local" special case. Figured it out on my own before finding this answer, but +1 for pointing this out, hopefully others find this before a lengthy debugging session.
    – flotzilla
    Feb 11, 2022 at 11:23
  • Bitten-by-Bash-local-case-counter: +1
    – Genzer
    Sep 22, 2022 at 5:14
set -e
source inner-scope.sh
echo $foo
echo "I thought I would've died :("

By adding echo, the subshell does not stand alone (is not separately checked) and does not abort. Assignment circumvents this problem.

You can also do this, and redirect the output to a file, to later process it.

tmpfile=$( mktemp )
inner > $tmpfile
cat $tmpfile
rm $tmpfile
  • Of course, the $tmpfile continues to exist in the second variant...
    – Daniel Beck
    Dec 1, 2011 at 13:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .