chepner's answer is the best solution: If you want to combine set -e (same as: set -o errexit) with an ERR trap, also use set -o errtrace (same as: set -E).
In short: use set -eE in lieu of just set -e:
#!/bin/bash
set -eE  # same as: `set -o errexit -o errtrace`
trap 'echo BOO!' ERR 
function func(){
  ls /root/
}
# Thanks to -E / -o errtrace, this still triggers the trap, 
# even though the failure occurs *inside the function*.
func 
A more sophisticated example trap example that prints the message in red and also prints the exit code:
trap 'printf "\e[31m%s: %s\e[m\n" "BOO!" $?' ERR
man bash says about set -o errtrace / set -E:
If set, any trap on ERR is inherited by shell functions, command substitutions, and commands executed in a subshell environment. The ERR trap is normally not inherited in such cases.
What I believe is happening:
- Without - -e: The- lscommand fails inside your function, and, due to being the last command in the function, the function reports- ls's nonzero exit code to the caller, your top-level script scope. In that scope, the- ERRtrap is in effect, and it is invoked (but note that execution will continue, unless you explicitly call- exitfrom the trap).
 
- With - -e(but without- -E): The- lscommand fails inside your function, and because- set -eis in effect, Bash instantly exits, directly from the function scope - and since there is no- ERRtrap in effect there (because it wasn't inherited from the parent scope), your trap is not called.
 
While the man page is not incorrect, I agree that this behavior is not exactly obvious - you have to infer it.