[bash] Is there a TRY CATCH command in Bash

I'm writing a shell script and need to check that a terminal app has been installed. I want to use a TRY/CATCH command to do this unless there is a neater way.

This question is related to bash shell error-handling

The answer is


I've developed an almost flawless try & catch implementation in bash, that allows you to write code like:

try 
    echo 'Hello'
    false
    echo 'This will not be displayed'

catch 
    echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"

You can even nest the try-catch blocks inside themselves!

try {
    echo 'Hello'

    try {
        echo 'Nested Hello'
        false
        echo 'This will not execute'
    } catch {
        echo "Nested Caught (@ $__EXCEPTION_LINE__)"
    }

    false
    echo 'This will not execute too'

} catch {
    echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
}

The code is a part of my bash boilerplate/framework. It further extends the idea of try & catch with things like error handling with backtrace and exceptions (plus some other nice features).

Here's the code that's responsible just for try & catch:

set -o pipefail
shopt -s expand_aliases
declare -ig __oo__insideTryCatch=0

# if try-catch is nested, then set +e before so the parent handler doesn't catch us
alias try="[[ \$__oo__insideTryCatch -gt 0 ]] && set +e;
           __oo__insideTryCatch+=1; ( set -e;
           trap \"Exception.Capture \${LINENO}; \" ERR;"
alias catch=" ); Exception.Extract \$? || "

Exception.Capture() {
    local script="${BASH_SOURCE[1]#./}"

    if [[ ! -f /tmp/stored_exception_source ]]; then
        echo "$script" > /tmp/stored_exception_source
    fi
    if [[ ! -f /tmp/stored_exception_line ]]; then
        echo "$1" > /tmp/stored_exception_line
    fi
    return 0
}

Exception.Extract() {
    if [[ $__oo__insideTryCatch -gt 1 ]]
    then
        set -e
    fi

    __oo__insideTryCatch+=-1

    __EXCEPTION_CATCH__=( $(Exception.GetLastException) )

    local retVal=$1
    if [[ $retVal -gt 0 ]]
    then
        # BACKWARDS COMPATIBILE WAY:
        # export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-1)]}"
        # export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-2)]}"
        export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[-1]}"
        export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[-2]}"
        export __EXCEPTION__="${__EXCEPTION_CATCH__[@]:0:(${#__EXCEPTION_CATCH__[@]} - 2)}"
        return 1 # so that we may continue with a "catch"
    fi
}

Exception.GetLastException() {
    if [[ -f /tmp/stored_exception ]] && [[ -f /tmp/stored_exception_line ]] && [[ -f /tmp/stored_exception_source ]]
    then
        cat /tmp/stored_exception
        cat /tmp/stored_exception_line
        cat /tmp/stored_exception_source
    else
        echo -e " \n${BASH_LINENO[1]}\n${BASH_SOURCE[2]#./}"
    fi

    rm -f /tmp/stored_exception /tmp/stored_exception_line /tmp/stored_exception_source
    return 0
}

Feel free to use, fork and contribute - it's on GitHub.


I can recommend this in "bash -ue" mode:

set -ue
   
false && RET=$? || RET=$? 
echo "expecting 1, got ${RET}"
true && RET=$? || RET=$? 
echo "expecting 0, got ${RET}"

echo "test try...catch"
false && RET=$? || RET=$? 
if [ ${RET} -ne 0 ]; then
  echo "caught error ${RET}"
fi

echo "beware, using '||' before '&&' fails"
echo "  -> memory aid: [A]nd before [O]r in the alphabet"
false || RET=$? && RET=$? 
echo "expecting 1, got ${RET}"
true || RET=$? && RET=$? 
echo "expecting 0, got ${RET}"

Assuming the terminal app in question is named 'app' (and you expect it to be in your $PATH):

if [[ ! `which app` ]]; then
    # run code if app not installed
else
    # run code if app is installed
fi

Below is an complete copy of the simplified script used in my other answer. Beyond additional error checking, there is an alias which allows the user to change the name of an existing alias. The syntax is given below. If the new_alias parameter is omitted, then the alias is removed.

ChangeAlias old_alias [new_alias]

The complete script is given below.

common.GetAlias() {
    local "oldname=${1:-0}"
    if [[ $oldname =~ ^[0-9]+$ && oldname+1 -lt ${#FUNCNAME[@]} ]]; then
        oldname="${FUNCNAME[oldname + 1]}"
    fi
    name="common_${oldname#common.}"
    echo "${!name:-$oldname}"
}

common.Alias() {
    if [[ $# -ne 2 || -z $1 || -z $2 ]]; then
        echo "$(common.GetAlias): The must be only two parameters of nonzero length" >&2
        return 1;
    fi
    eval "alias $1='$2'"
    local "f=${2##*common.}"
    f="${f%%;*}"
    local "v=common_$f"
    f="common.$f"
    if [[ -n ${!v:-} ]]; then
        echo "$(common.GetAlias): $1: Function \`$f' already paired with name \`${!v}'" >&2
        return 1;
    fi
    shopt -s expand_aliases
    eval "$v=\"$1\""
}

common.ChangeAlias() {
    if [[ $# -lt 1 || $# -gt 2 ]]; then
        echo "usage: $(common.GetAlias) old_name [new_name]" >&2
        return "1"
    elif ! alias "$1" &>"/dev/null"; then
        echo "$(common.GetAlias): $1: Name not found" >&2
        return 1;
    fi
    local "s=$(alias "$1")" 
    s="${s#alias $1=\'}"
    s="${s%\'}"
    local "f=${s##*common.}"
    f="${f%%;*}"
    local "v=common_$f"
    f="common.$f"
    if [[ ${!v:-} != "$1" ]]; then
        echo "$(common.GetAlias): $1: Name not paired with a function \`$f'" >&2
        return 1;
    elif [[ $# -gt 1 ]]; then
        eval "alias $2='$s'"
        eval "$v=\"$2\""
    else
        unset "$v"
    fi
    unalias "$1"
}

common.Alias exception             'common.Exception'
common.Alias throw                 'common.Throw'
common.Alias try                   '{ if common.Try; then'
common.Alias yrt                   'common.EchoExitStatus; fi; common.yrT; }'
common.Alias catch                 '{ while common.Catch'
common.Alias hctac                 'common.hctaC -r; done; common.hctaC; }'
common.Alias finally               '{ if common.Finally; then'
common.Alias yllanif               'fi; common.yllaniF; }'
common.Alias caught                'common.Caught'
common.Alias EchoExitStatus        'common.EchoExitStatus'
common.Alias EnableThrowOnError    'common.EnableThrowOnError'
common.Alias DisableThrowOnError   'common.DisableThrowOnError'
common.Alias GetStatus             'common.GetStatus'
common.Alias SetStatus             'common.SetStatus'
common.Alias GetMessage            'common.GetMessage'
common.Alias MessageCount          'common.MessageCount'
common.Alias CopyMessages          'common.CopyMessages'
common.Alias TryCatchFinally       'common.TryCatchFinally'
common.Alias DefaultErrHandler     'common.DefaultErrHandler'
common.Alias DefaultUnhandled      'common.DefaultUnhandled'
common.Alias CallStack             'common.CallStack'
common.Alias ChangeAlias           'common.ChangeAlias'
common.Alias TryCatchFinallyAlias  'common.Alias'

common.CallStack() {
    local -i "i" "j" "k" "subshell=${2:-0}" "wi" "wl" "wn"
    local "format= %*s  %*s  %-*s  %s\n" "name"
    eval local "lineno=('' ${BASH_LINENO[@]})"
    for (( i=${1:-0},j=wi=wl=wn=0; i<${#FUNCNAME[@]}; ++i,++j )); do  
        name="$(common.GetAlias "$i")"
        let "wi = ${#j} > wi ? wi = ${#j} : wi"
        let "wl = ${#lineno[i]} > wl ? wl = ${#lineno[i]} : wl"
        let "wn = ${#name} > wn ? wn = ${#name} : wn"
    done
    for (( i=${1:-0},j=0; i<${#FUNCNAME[@]}; ++i,++j )); do
        ! let "k = ${#FUNCNAME[@]} - i - 1"
        name="$(common.GetAlias "$i")"
        printf "$format" "$wi" "$j" "$wl" "${lineno[i]}" "$wn" "$name" "${BASH_SOURCE[i]}"
    done
}

common.Echo() {
    [[ $common_options != *d* ]] || echo "$@" >"$common_file"
}

common.DefaultErrHandler() {
    echo "Orginal Status: $common_status"
    echo "Exception Type: ERR"
}

common.Exception() {
    common.TryCatchFinallyVerify || return
    if [[ $# -eq 0 ]]; then
        echo "$(common.GetAlias): At least one parameter is required" >&2
        return "1"         
    elif [[ ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -lt 1 || 10#$1 -gt 255 ]]; then
        echo "$(common.GetAlias): $1: First parameter was not an integer between 1 and 255" >&2
        return "1"
    fi
    let "common_status = 10#$1"
    shift
    common_messages=()
    for message in "$@"; do
        common_messages+=("$message")
    done
    if [[ $common_options == *c* ]]; then
        echo "Call Stack:" >"$common_fifo"
        common.CallStack "2" >"$common_fifo"
    fi
}

common.Throw() {
    common.TryCatchFinallyVerify || return
    local "message"
    if ! common.TryCatchFinallyExists; then
        echo "$(common.GetAlias): No Try-Catch-Finally exists" >&2
        return "1"        
    elif [[ $# -eq 0 && common_status -eq 0 ]]; then
        echo "$(common.GetAlias): No previous unhandled exception" >&2 
        return "1"
    elif [[ $# -gt 0 && ( ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -lt 1 || 10#$1 -gt 255 ) ]]; then
        echo "$(common.GetAlias): $1: First parameter was not an integer between 1 and 255" >&2
        return "1"
    fi
    common.Echo -n "In Throw ?=$common_status "
    common.Echo "try=$common_trySubshell subshell=$BASH_SUBSHELL #=$#"
    if [[ $common_options == *k* ]]; then
        common.CallStack "2" >"$common_file"
    fi
    if [[ $# -gt 0 ]]; then
        let "common_status = 10#$1"
        shift
        for message in "$@"; do
            echo "$message" >"$common_fifo"
        done
        if [[ $common_options == *c* ]]; then
            echo "Call Stack:" >"$common_fifo"
            common.CallStack "2" >"$common_fifo"
        fi
    elif [[ ${#common_messages[@]} -gt 0 ]]; then
        for message in "${common_messages[@]}"; do
            echo "$message" >"$common_fifo"
        done
    fi
    chmod "0400" "$common_fifo"
    common.Echo "Still in Throw $=$common_status subshell=$BASH_SUBSHELL #=$# -=$-"
    exit "$common_status"
}

common.ErrHandler() {
    common_status=$?
    trap ERR
    common.Echo -n "In ErrHandler ?=$common_status debug=$common_options "
    common.Echo "try=$common_trySubshell subshell=$BASH_SUBSHELL order=$common_order"
    if [[ -w "$common_fifo" ]]; then
        if [[ $common_options != *e* ]]; then
            common.Echo "ErrHandler is ignoring"
            common_status="0"
            return "$common_status" # value is ignored
        fi
        if [[ $common_options == *k* ]]; then
            common.CallStack "2" >"$common_file"
        fi
        common.Echo "Calling ${common_errHandler:-}"
        eval "${common_errHandler:-} \"${BASH_LINENO[0]}\" \"${BASH_SOURCE[1]}\" \"${FUNCNAME[1]}\" >$common_fifo <$common_fifo"
        if [[ $common_options == *c* ]]; then
            echo "Call Stack:" >"$common_fifo"
            common.CallStack "2" >"$common_fifo"
        fi
        chmod "0400" "$common_fifo"
    fi
    common.Echo "Still in ErrHandler $=$common_status subshell=$BASH_SUBSHELL -=$-"
    if [[ common_trySubshell -eq BASH_SUBSHELL ]]; then
        return "$common_status" # value is ignored   
    else
        exit "$common_status"
    fi
}

common.Token() {
    local "name"
    case $1 in
    b) name="before";;
    t) name="$common_Try";;
    y) name="$common_yrT";;
    c) name="$common_Catch";;
    h) name="$common_hctaC";;
    f) name="$common_yllaniF";;
    l) name="$common_Finally";;
    *) name="unknown";;
    esac
    echo "$name"
}

common.TryCatchFinallyNext() {
    common.ShellInit
    local "previous=$common_order" "errmsg"
    common_order="$2"
    if [[ $previous != $1 ]]; then
        errmsg="${BASH_SOURCE[2]}: line ${BASH_LINENO[1]}: syntax error_near unexpected token \`$(common.Token "$2")'"
        echo "$errmsg" >&2
        [[ /dev/fd/2 -ef $common_file ]] || echo "$errmsg" >"$common_file"
        kill -s INT 0
        return "1"        
    fi
}

common.ShellInit() {
    if [[ common_initSubshell -ne BASH_SUBSHELL ]]; then
        common_initSubshell="$BASH_SUBSHELL"
        common_order="b"
    fi
}

common.Try() {
    common.TryCatchFinallyVerify || return
    common.TryCatchFinallyNext "[byhl]" "t" || return 
    common_status="0"
    common_subshell="$common_trySubshell"
    common_trySubshell="$BASH_SUBSHELL"
    common_messages=()
    common.Echo "-------------> Setting try=$common_trySubshell at subshell=$BASH_SUBSHELL"
}

common.yrT() {
    local "status=$?"
    common.TryCatchFinallyVerify || return
    common.TryCatchFinallyNext "[t]" "y" || return 
    common.Echo -n "Entered yrT ?=$status status=$common_status "
    common.Echo "try=$common_trySubshell subshell=$BASH_SUBSHELL"
    if [[ common_status -ne 0 ]]; then    

        common.Echo "Build message array. ?=$common_status, subshell=$BASH_SUBSHELL"
        local "message=" "eof=TRY_CATCH_FINALLY_END_OF_MESSAGES_$RANDOM"
        chmod "0600" "$common_fifo"
        echo "$eof" >"$common_fifo"
        common_messages=()
        while read "message"; do

            common.Echo "----> $message"

            [[ $message != *$eof ]] || break
            common_messages+=("$message")
        done <"$common_fifo"
    fi

    common.Echo "In ytT status=$common_status"
    common_trySubshell="$common_subshell"
}

common.Catch() {
    common.TryCatchFinallyVerify || return
    common.TryCatchFinallyNext "[yh]" "c" || return 
    [[ common_status -ne 0 ]] || return "1"
    local "parameter" "pattern" "value"
    local "toggle=true" "compare=p" "options=$-"
    local -i "i=-1" "status=0"
    set -f
    for parameter in "$@"; do
        if "$toggle"; then
            toggle="false"
            if [[ $parameter =~ ^-[notepr]$ ]]; then
                compare="${parameter#-}"
                continue 
            fi
        fi
        toggle="true"
        while "true"; do
            eval local "patterns=($parameter)"
            if [[ ${#patterns[@]} -gt 0 ]]; then
                for pattern in "${patterns[@]}"; do
                    [[ i -lt ${#common_messages[@]} ]] || break
                    if [[ i -lt 0 ]]; then
                        value="$common_status"
                    else
                        value="${common_messages[i]}"
                    fi
                    case $compare in
                    [ne]) [[ ! $value == "$pattern" ]] || break 2;;
                    [op]) [[ ! $value == $pattern ]] || break 2;;
                    [tr]) [[ ! $value =~ $pattern ]] || break 2;;
                    esac
                done
            fi
            if [[ $compare == [not] ]]; then
                let "++i,1"
                continue 2
            else
                status="1"
                break 2
            fi
        done
        if [[ $compare == [not] ]]; then
            status="1"
            break
        else
            let "++i,1"
        fi
    done
    [[ $options == *f* ]] || set +f
    return "$status"
} 

common.hctaC() {
    common.TryCatchFinallyVerify || return
    common.TryCatchFinallyNext "[c]" "h" || return 
    [[ $# -ne 1 || $1 != -r ]] || common_status="0"
}

common.Finally() {
    common.TryCatchFinallyVerify || return
    common.TryCatchFinallyNext "[ych]" "f" || return 
}

common.yllaniF() {
    common.TryCatchFinallyVerify || return
    common.TryCatchFinallyNext "[f]" "l" || return 
    [[ common_status -eq 0 ]] || common.Throw
}

common.Caught() {
    common.TryCatchFinallyVerify || return
    [[ common_status -eq 0 ]] || return 1
}

common.EchoExitStatus() {
    return "${1:-$?}"
}

common.EnableThrowOnError() {
    common.TryCatchFinallyVerify || return
    [[ $common_options == *e* ]] || common_options+="e"
}

common.DisableThrowOnError() {
    common.TryCatchFinallyVerify || return
    common_options="${common_options/e}"
}

common.GetStatus() {
    common.TryCatchFinallyVerify || return
    echo "$common_status"
}

common.SetStatus() {
    common.TryCatchFinallyVerify || return
    if [[ $# -ne 1 ]]; then
        echo "$(common.GetAlias): $#: Wrong number of parameters" >&2
        return "1"         
    elif [[ ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -lt 1 || 10#$1 -gt 255 ]]; then
        echo "$(common.GetAlias): $1: First parameter was not an integer between 1 and 255" >&2
        return "1"
    fi
    let "common_status = 10#$1"
}

common.GetMessage() {
    common.TryCatchFinallyVerify || return
    local "upper=${#common_messages[@]}"
    if [[ upper -eq 0 ]]; then
        echo "$(common.GetAlias): $1: There are no messages" >&2
        return "1"
    elif [[ $# -ne 1 ]]; then
        echo "$(common.GetAlias): $#: Wrong number of parameters" >&2
        return "1"         
    elif [[ ${#1} -gt 16 || -n ${1%%[0-9]*} || 10#$1 -ge upper ]]; then
        echo "$(common.GetAlias): $1: First parameter was an invalid index" >&2
        return "1"
    fi
    echo "${common_messages[$1]}"
}

common.MessageCount() {
    common.TryCatchFinallyVerify || return
    echo "${#common_messages[@]}"
}

common.CopyMessages() {
    common.TryCatchFinallyVerify || return
    if [[ $# -ne 1 ]]; then
        echo "$(common.GetAlias): $#: Wrong number of parameters" >&2
        return "1"         
    elif [[ ${#common_messages} -gt 0 ]]; then
        eval "$1=(\"\${common_messages[@]}\")"
    else
        eval "$1=()"
    fi
}

common.TryCatchFinallyExists() {
    [[ ${common_fifo:-u} != u ]]
}

common.TryCatchFinallyVerify() {
    local "name"
    if ! common.TryCatchFinallyExists; then
        echo "$(common.GetAlias "1"): No Try-Catch-Finally exists" >&2
        return "2"        
    fi
}

common.GetOptions() {
    local "opt"
    local "name=$(common.GetAlias "1")"
    if common.TryCatchFinallyExists; then
        echo "$name: A Try-Catch-Finally already exists" >&2
        return "1"        
    fi
    let "OPTIND = 1"
    let "OPTERR = 0"
    while getopts ":cdeh:ko:u:v:" opt "$@"; do
        case $opt in
        c)  [[ $common_options == *c* ]] || common_options+="c";;
        d)  [[ $common_options == *d* ]] || common_options+="d";;
        e)  [[ $common_options == *e* ]] || common_options+="e";;
        h)  common_errHandler="$OPTARG";;
        k)  [[ $common_options == *k* ]] || common_options+="k";;
        o)  common_file="$OPTARG";;
        u)  common_unhandled="$OPTARG";;
        v)  common_command="$OPTARG";;
        \?) #echo "Invalid option: -$OPTARG" >&2
            echo "$name: Illegal option: $OPTARG" >&2
            return "1";;
        :)  echo "$name: Option requires an argument: $OPTARG" >&2
            return "1";;
        *)  echo "$name: An error occurred while parsing options." >&2
            return "1";;
        esac
    done

    shift "$((OPTIND - 1))"
    if [[ $# -lt 1 ]]; then
        echo "$name: The fifo_file parameter is missing" >&2
        return "1"
    fi
    common_fifo="$1"
    if [[ ! -p $common_fifo ]]; then
        echo "$name: $1: The fifo_file is not an open FIFO" >&2
        return "1"  
    fi

    shift
    if [[ $# -lt 1 ]]; then
        echo "$name: The function parameter is missing" >&2
        return "1"
    fi
    common_function="$1"
    if ! chmod "0600" "$common_fifo"; then
        echo "$name: $common_fifo: Can not change file mode to 0600" >&2
        return "1"
    fi

    local "message=" "eof=TRY_CATCH_FINALLY_END_OF_FILE_$RANDOM"
    { echo "$eof" >"$common_fifo"; } 2>"/dev/null"
    if [[ $? -ne 0 ]]; then
        echo "$name: $common_fifo: Can not write" >&2
        return "1"
    fi   
    { while [[ $message != *$eof ]]; do
        read "message"
    done <"$common_fifo"; } 2>"/dev/null"
    if [[ $? -ne 0 ]]; then
        echo "$name: $common_fifo: Can not read" >&2
        return "1"
    fi   

    return "0"
}

common.DefaultUnhandled() {
    local -i "i"
    echo "-------------------------------------------------"
    echo "$(common.GetAlias "common.TryCatchFinally"): Unhandeled exception occurred"
    echo "Status: $(GetStatus)"
    echo "Messages:"
    for ((i=0; i<$(MessageCount); i++)); do
        echo "$(GetMessage "$i")"
    done
    echo "-------------------------------------------------"
}

common.TryCatchFinally() {
    local "common_file=/dev/fd/2"
    local "common_errHandler=common.DefaultErrHandler"
    local "common_unhandled=common.DefaultUnhandled"
    local "common_options="
    local "common_fifo="
    local "common_function="
    local "common_flags=$-"
    local "common_trySubshell=-1"
    local "common_initSubshell=-1"
    local "common_subshell"
    local "common_status=0"
    local "common_order=b"
    local "common_command="
    local "common_messages=()"
    local "common_handler=$(trap -p ERR)"
    [[ -n $common_handler ]] || common_handler="trap ERR"

    common.GetOptions "$@" || return "$?"
    shift "$((OPTIND + 1))"

    [[ -z $common_command ]] || common_command+="=$"
    common_command+='("$common_function" "$@")'

    set -E
    set +e
    trap "common.ErrHandler" ERR
    if true; then
        common.Try 
        eval "$common_command"
        common.EchoExitStatus
        common.yrT
    fi
    while common.Catch; do
        "$common_unhandled" >&2
        break
        common.hctaC -r
    done
    common.hctaC
    [[ $common_flags == *E* ]] || set +E
    [[ $common_flags != *e* ]] || set -e
    [[ $common_flags != *f* || $- == *f* ]] || set -f
    [[ $common_flags == *f* || $- != *f* ]] || set +f
    eval "$common_handler"
    return "$((common_status?2:0))"
}

You can use trap:

try { block A } catch { block B } finally { block C }

translates to:

(
  set -Ee
  function _catch {
    block B
    exit 0  # optional; use if you don't want to propagate (rethrow) error to outer shell
  }
  function _finally {
    block C
  }
  trap _catch ERR
  trap _finally EXIT
  block A
)

There are so many similar solutions which probably work. Below is a simple and working way to accomplish try/catch, with explanation in the comments.

#!/bin/bash

function a() {
  # do some stuff here
}
function b() {
  # do more stuff here
}

# this subshell is a scope of try
# try
(
  # this flag will make to exit from current subshell on any error
  # inside it (all functions run inside will also break on any error)
  set -e
  a
  b
  # do more stuff here
)
# and here we catch errors
# catch
errorCode=$?
if [ $errorCode -ne 0 ]; then
  echo "We have an error"
  # We exit the all script with the same error, if you don't want to
  # exit it and continue, just delete this line.
  exit $errorCode
fi

Based on some answers I found here, I made myself a small helper file to source for my projects:

trycatch.sh

#!/bin/bash

function try()
{
    [[ $- = *e* ]]; SAVED_OPT_E=$?
    set +e
}

function throw()
{
    exit $1
}

function catch()
{
    export ex_code=$?
    (( $SAVED_OPT_E )) && set +e
    return $ex_code
}

function throwErrors()
{
    set -e
}

function ignoreErrors()
{
    set +e
}

here is an example how it looks like in use:

#!/bin/bash
export AnException=100
export AnotherException=101

# start with a try
try
(   # open a subshell !!!
    echo "do something"
    [ someErrorCondition ] && throw $AnException

    echo "do something more"
    executeCommandThatMightFail || throw $AnotherException

    throwErrors # automaticatly end the try block, if command-result is non-null
    echo "now on to something completely different"
    executeCommandThatMightFail

    echo "it's a wonder we came so far"
    executeCommandThatFailsForSure || true # ignore a single failing command

    ignoreErrors # ignore failures of commands until further notice
    executeCommand1ThatFailsForSure
    local result = $(executeCommand2ThatFailsForSure)
    [ result != "expected error" ] && throw $AnException # ok, if it's not an expected error, we want to bail out!
    executeCommand3ThatFailsForSure

    echo "finished"
)
# directly after closing the subshell you need to connect a group to the catch using ||
catch || {
    # now you can handle
    case $ex_code in
        $AnException)
            echo "AnException was thrown"
        ;;
        $AnotherException)
            echo "AnotherException was thrown"
        ;;
        *)
            echo "An unexpected exception was thrown"
            throw $ex_code # you can rethrow the "exception" causing the script to exit if not caught
        ;;
    esac
}

Below is an example of a script which implements try/catch/finally in bash.

Like other answers to this question, exceptions must be caught after exiting a subprocess.

The example scripts start by creating an anonymous fifo, which is used to pass string messages from a command exception or throw to end of the closest try block. Here the messages are removed from the fifo and placed in an array variable. The status is returned through return and exit commands and placed in a different variable. To enter a catch block, this status must not be zero. Other requirements to enter a catch block are passed as parameters. If the end of a catch block is reached, then the status is set to zero. If the end of the finally block is reached and the status is still nonzero, then an implicit throw containing the messages and status is executed. The script requires the calling of the function trycatchfinally which contains an unhandled exception handler.

The syntax for the trycatchfinally command is given below.

trycatchfinally [-cde] [-h ERR_handler] [-k] [-o debug_file] [-u unhandled_handler] [-v variable] fifo function

The -c option adds the call stack to the exception messages.
The -d option enables debug output.
The -e option enables command exceptions.
The -h option allows the user to substitute their own command exception handler.
The -k option adds the call stack to the debug output.
The -o option replaces the default output file which is /dev/fd/2.
The -u option allows the user to substitute their own unhandled exception handler.
The -v option allows the user the option to pass back values though the use of Command Substitution.
The fifo is the fifo filename.
The function function is called by trycatchfinally as a subprocess.

Note: The cdko options were removed to simplify the script.

The syntax for the catch command is given below.

catch [[-enoprt] list ...] ...

The options are defined below. The value for the first list is the status. Subsquent values are the messages. If the there are more messages than lists, then the remaining messages are ignored.

-e means [[ $value == "$string" ]] (the value has to match at least one string in the list)
-n means [[ $value != "$string" ]] (the value can not match any of the strings in the list)
-o means [[ $value != $pattern ]] (the value can not match any of the patterns in the list)
-p means [[ $value == $pattern ]] (the value has to match at least one pattern in the list)
-r means [[ $value =~ $regex ]] (the value has to match at least one extended regular expression in the list)
-t means [[ ! $value =~ $regex ]] (the value can not match any of the extended regular expressions in the list)

The try/catch/finally script is given below. To simplify the script for this answer, most of the error checking was removed. This reduced the size by 64%. A complete copy of this script can be found at my other answer.

shopt -s expand_aliases
alias try='{ common.Try'
alias yrt='EchoExitStatus; common.yrT; }'
alias catch='{ while common.Catch'
alias hctac='common.hctaC; done; }'
alias finally='{ common.Finally'
alias yllanif='common.yllaniF; }'

DefaultErrHandler() {
    echo "Orginal Status: $common_status"
    echo "Exception Type: ERR"
}

exception() {
    let "common_status = 10#$1"
    shift
    common_messages=()
    for message in "$@"; do
        common_messages+=("$message")
    done
}

throw() {
    local "message"
    if [[ $# -gt 0 ]]; then
        let "common_status = 10#$1"
        shift
        for message in "$@"; do
            echo "$message" >"$common_fifo"
        done
    elif [[ ${#common_messages[@]} -gt 0 ]]; then
        for message in "${common_messages[@]}"; do
            echo "$message" >"$common_fifo"
        done
    fi
    chmod "0400" "$common_fifo"
    exit "$common_status"
}

common.ErrHandler() {
    common_status=$?
    trap ERR
    if [[ -w "$common_fifo" ]]; then
        if [[ $common_options != *e* ]]; then
            common_status="0"
            return
        fi
        eval "${common_errHandler:-} \"${BASH_LINENO[0]}\" \"${BASH_SOURCE[1]}\" \"${FUNCNAME[1]}\" >$common_fifo <$common_fifo"
        chmod "0400" "$common_fifo"
    fi
    if [[ common_trySubshell -eq BASH_SUBSHELL ]]; then
        return   
    else
        exit "$common_status"
    fi
}

common.Try() {
    common_status="0"
    common_subshell="$common_trySubshell"
    common_trySubshell="$BASH_SUBSHELL"
    common_messages=()
}

common.yrT() {
    local "status=$?"
    if [[ common_status -ne 0 ]]; then    
        local "message=" "eof=TRY_CATCH_FINALLY_END_OF_MESSAGES_$RANDOM"
        chmod "0600" "$common_fifo"
        echo "$eof" >"$common_fifo"
        common_messages=()
        while read "message"; do
            [[ $message != *$eof ]] || break
            common_messages+=("$message")
        done <"$common_fifo"
    fi
    common_trySubshell="$common_subshell"
}

common.Catch() {
    [[ common_status -ne 0 ]] || return "1"
    local "parameter" "pattern" "value"
    local "toggle=true" "compare=p" "options=$-"
    local -i "i=-1" "status=0"
    set -f
    for parameter in "$@"; do
        if "$toggle"; then
            toggle="false"
            if [[ $parameter =~ ^-[notepr]$ ]]; then
                compare="${parameter#-}"
                continue 
            fi
        fi
        toggle="true"
        while "true"; do
            eval local "patterns=($parameter)"
            if [[ ${#patterns[@]} -gt 0 ]]; then
                for pattern in "${patterns[@]}"; do
                    [[ i -lt ${#common_messages[@]} ]] || break
                    if [[ i -lt 0 ]]; then
                        value="$common_status"
                    else
                        value="${common_messages[i]}"
                    fi
                    case $compare in
                    [ne]) [[ ! $value == "$pattern" ]] || break 2;;
                    [op]) [[ ! $value == $pattern ]] || break 2;;
                    [tr]) [[ ! $value =~ $pattern ]] || break 2;;
                    esac
                done
            fi
            if [[ $compare == [not] ]]; then
                let "++i,1"
                continue 2
            else
                status="1"
                break 2
            fi
        done
        if [[ $compare == [not] ]]; then
            status="1"
            break
        else
            let "++i,1"
        fi
    done
    [[ $options == *f* ]] || set +f
    return "$status"
} 

common.hctaC() {
    common_status="0"
}

common.Finally() {
    :
}

common.yllaniF() {
    [[ common_status -eq 0 ]] || throw
}

caught() {
    [[ common_status -eq 0 ]] || return 1
}

EchoExitStatus() {
    return "${1:-$?}"
}

EnableThrowOnError() {
    [[ $common_options == *e* ]] || common_options+="e"
}

DisableThrowOnError() {
    common_options="${common_options/e}"
}

GetStatus() {
    echo "$common_status"
}

SetStatus() {
    let "common_status = 10#$1"
}

GetMessage() {
    echo "${common_messages[$1]}"
}

MessageCount() {
    echo "${#common_messages[@]}"
}

CopyMessages() {
    if [[ ${#common_messages} -gt 0 ]]; then
        eval "$1=(\"\${common_messages[@]}\")"
    else
        eval "$1=()"
    fi
}

common.GetOptions() {
    local "opt"
    let "OPTIND = 1"
    let "OPTERR = 0"
    while getopts ":cdeh:ko:u:v:" opt "$@"; do
        case $opt in
        e)  [[ $common_options == *e* ]] || common_options+="e";;
        h)  common_errHandler="$OPTARG";;
        u)  common_unhandled="$OPTARG";;
        v)  common_command="$OPTARG";;
        esac
    done
    shift "$((OPTIND - 1))"
    common_fifo="$1"
    shift
    common_function="$1"
    chmod "0600" "$common_fifo"
}

DefaultUnhandled() {
    local -i "i"
    echo "-------------------------------------------------"
    echo "TryCatchFinally: Unhandeled exception occurred"
    echo "Status: $(GetStatus)"
    echo "Messages:"
    for ((i=0; i<$(MessageCount); i++)); do
        echo "$(GetMessage "$i")"
    done
    echo "-------------------------------------------------"
}

TryCatchFinally() {
    local "common_errHandler=DefaultErrHandler"
    local "common_unhandled=DefaultUnhandled"
    local "common_options="
    local "common_fifo="
    local "common_function="
    local "common_flags=$-"
    local "common_trySubshell=-1"
    local "common_subshell"
    local "common_status=0"
    local "common_command="
    local "common_messages=()"
    local "common_handler=$(trap -p ERR)"
    [[ -n $common_handler ]] || common_handler="trap ERR"
    common.GetOptions "$@"
    shift "$((OPTIND + 1))"
    [[ -z $common_command ]] || common_command+="=$"
    common_command+='("$common_function" "$@")'
    set -E
    set +e
    trap "common.ErrHandler" ERR
    try
        eval "$common_command"
    yrt
    catch; do
        "$common_unhandled" >&2
    hctac
    [[ $common_flags == *E* ]] || set +E
    [[ $common_flags != *e* ]] || set -e
    [[ $common_flags != *f* || $- == *f* ]] || set -f
    [[ $common_flags == *f* || $- != *f* ]] || set +f
    eval "$common_handler"
}

Below is an example, which assumes the above script is stored in the file named simple. The makefifo file contains the script described in this answer. The assumption is made that the file named 4444kkkkk does not exist, therefore causing an exception to occur. The error message output from the ls 4444kkkkk command is automatically suppressed until inside the appropriate catch block.

#!/bin/bash
#

if [[ $0 != ${BASH_SOURCE[0]} ]]; then
    bash "${BASH_SOURCE[0]}" "$@"
    return
fi

source simple
source makefifo

MyFunction3() {
    echo "entered MyFunction3" >&4
    echo "This is from MyFunction3"
    ls 4444kkkkk
    echo "leaving MyFunction3" >&4
}

MyFunction2() {
    echo "entered MyFunction2" >&4
    value="$(MyFunction3)"
    echo "leaving MyFunction2" >&4
}

MyFunction1() {
    echo "entered MyFunction1" >&4
    local "flag=false"
    try 
    (
        echo "start of try" >&4
        MyFunction2
        echo "end of try" >&4
    )
    yrt
    catch "[1-3]" "*" "Exception\ Type:\ ERR"; do
        echo 'start of catch "[1-3]" "*" "Exception\ Type:\ ERR"'
        local -i "i"
        echo "-------------------------------------------------"
        echo "Status: $(GetStatus)"
        echo "Messages:"
        for ((i=0; i<$(MessageCount); i++)); do
            echo "$(GetMessage "$i")"
        done
        echo "-------------------------------------------------"
        break
        echo 'end of catch "[1-3]" "*" "Exception\ Type:\ ERR"'
    hctac >&4
    catch "1 3 5" "*" -n "Exception\ Type:\ ERR"; do
        echo 'start of catch "1 3 5" "*" -n "Exception\ Type:\ ERR"'
        echo "-------------------------------------------------"
        echo "Status: $(GetStatus)"
        [[ $(MessageCount) -le 1 ]] || echo "$(GetMessage "1")"
        echo "-------------------------------------------------"
        break
        echo 'end of catch "1 3 5" "*" -n "Exception\ Type:\ ERR"'
    hctac >&4
    catch; do
        echo 'start of catch' >&4
        echo "failure"
        flag="true"
        echo 'end of catch' >&4
    hctac
    finally
        echo "in finally"
    yllanif >&4
    "$flag" || echo "success"
    echo "leaving MyFunction1" >&4
} 2>&6

ErrHandler() {
    echo "EOF"
    DefaultErrHandler "$@"
    echo "Function: $3"
    while read; do
        [[ $REPLY != *EOF ]] || break
        echo "$REPLY"
    done
}

set -u
echo "starting" >&2
MakeFIFO "6"
TryCatchFinally -e -h ErrHandler -o /dev/fd/4 -v result /dev/fd/6 MyFunction1 4>&2
echo "result=$result"
exec >&6-

The above script was tested using GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). The output, from running this script, is shown below.

starting
entered MyFunction1
start of try
entered MyFunction2
entered MyFunction3
start of catch "[1-3]" "*" "Exception\ Type:\ ERR"
-------------------------------------------------
Status: 1
Messages:
Orginal Status: 1
Exception Type: ERR
Function: MyFunction3
ls: 4444kkkkk: No such file or directory
-------------------------------------------------
start of catch
end of catch
in finally
leaving MyFunction1
result=failure

Another example which uses a throw can be created by replacing function MyFunction3 with the script shown below.

MyFunction3() {
    echo "entered MyFunction3" >&4
    echo "This is from MyFunction3"
    throw "3" "Orginal Status: 3" "Exception Type: throw"
    echo "leaving MyFunction3" >&4
}

The syntax for the throw command is given below. If no parameters are present, then status and messages stored in the variables are used instead.

throw [status] [message ...]

The output, from executing the modified script, is shown below.

starting
entered MyFunction1
start of try
entered MyFunction2
entered MyFunction3
start of catch "1 3 5" "*" -n "Exception\ Type:\ ERR"
-------------------------------------------------
Status: 3
Exception Type: throw
-------------------------------------------------
start of catch
end of catch
in finally
leaving MyFunction1
result=failure

A very simple thing I use:

try() {
    "$@" || (e=$?; echo "$@" > /dev/stderr; exit $e)
}

And you have traps http://www.tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html which is not the same, but other technique you can use for this purpose


bash does not abort the running execution in case something detects an error state (unless you set the -e flag). Programming languages which offer try/catch do this in order to inhibit a "bailing out" because of this special situation (hence typically called "exception").

In the bash, instead, only the command in question will exit with an exit code greater than 0, indicating that error state. You can check for that of course, but since there is no automatic bailing out of anything, a try/catch does not make sense. It is just lacking that context.

You can, however, simulate a bailing out by using sub shells which can terminate at a point you decide:

(
  echo "Do one thing"
  echo "Do another thing"
  if some_condition
  then
    exit 3  # <-- this is our simulated bailing out
  fi
  echo "Do yet another thing"
  echo "And do a last thing"
)   # <-- here we arrive after the simulated bailing out, and $? will be 3 (exit code)
if [ $? = 3 ]
then
  echo "Bail out detected"
fi

Instead of that some_condition with an if you also can just try a command, and in case it fails (has an exit code greater than 0), bail out:

(
  echo "Do one thing"
  echo "Do another thing"
  some_command || exit 3
  echo "Do yet another thing"
  echo "And do a last thing"
)
...

Unfortunately, using this technique you are restricted to 255 different exit codes (1..255) and no decent exception objects can be used.

If you need more information to pass along with your simulated exception, you can use the stdout of the subshells, but that is a bit complicated and maybe another question ;-)

Using the above mentioned -e flag to the shell you can even strip that explicit exit statement:

(
  set -e
  echo "Do one thing"
  echo "Do another thing"
  some_command
  echo "Do yet another thing"
  echo "And do a last thing"
)
...

You can do:

#!/bin/bash
if <command> ; then # TRY
    <do-whatever-you-want>
else # CATCH
    echo 'Exception'
    <do-whatever-you-want>
fi

As everybody says, bash doesn't have a proper language-supported try/catch syntax. You can launch bash with the -e argument or use set -e inside the script to abort the entire bash process if any command has a non-zero exit code. (You can also set +e to temporarily allow failing commands.)

So, one technique to simulate a try/catch block is to launch a sub-process to do the work with -e enabled. Then in the main process, check the return code of the sub-process.

Bash supports heredoc strings, so you don't have to write two separate files to handle this. In the below example, the TRY heredoc will run in a separate bash instance, with -e enabled, so the sub-process will crash if any command returns a non-zero exit code. Then, back in the main process, we can check the return code to handle a catch block.

#!/bin/bash

set +e
bash -e <<TRY
  echo hello
  cd /does/not/exist
  echo world
TRY
if [ $? -ne 0 ]; then
  echo caught exception
fi

It's not a proper language-supported try/catch block, but it may scratch a similar itch for you.


Examples related to bash

Comparing a variable with a string python not working when redirecting from bash script Zipping a file in bash fails How do I prevent Conda from activating the base environment by default? Get first line of a shell command's output Fixing a systemd service 203/EXEC failure (no such file or directory) /bin/sh: apt-get: not found VSCode Change Default Terminal Run bash command on jenkins pipeline How to check if the docker engine and a docker container are running? How to switch Python versions in Terminal?

Examples related to shell

Comparing a variable with a string python not working when redirecting from bash script Get first line of a shell command's output How to run shell script file using nodejs? Run bash command on jenkins pipeline Way to create multiline comments in Bash? How to do multiline shell script in Ansible How to check if a file exists in a shell script How to check if an environment variable exists and get its value? Curl to return http status code along with the response docker entrypoint running bash script gets "permission denied"

Examples related to error-handling

must declare a named package eclipse because this compilation unit is associated to the named module Error:Failed to open zip file. Gradle's dependency cache may be corrupt What does 'index 0 is out of bounds for axis 0 with size 0' mean? What's the source of Error: getaddrinfo EAI_AGAIN? Error handling with try and catch in Laravel What does "Fatal error: Unexpectedly found nil while unwrapping an Optional value" mean? Raise error in a Bash script Javascript Uncaught TypeError: Cannot read property '0' of undefined Multiple values in single-value context IndexError: too many indices for array