'trap cleanup ERR' installs a handler in the current shell only. Shell functions, command substitutions, and subshells do not inherit it, so a failure inside a function never reaches the handler and the script appears to skip its error path. The same restriction applies to the DEBUG and RETURN traps.
'set -E', also spelled 'set -o errtrace', makes the ERR trap inherited by functions, command substitutions, and subshell commands. 'set -T', or 'set -o functrace', does the same for DEBUG and RETURN. The common hardened preamble is 'set -Eeuo pipefail' together with an ERR trap that prints the failing line via the LINENO and BASH_COMMAND variables.
The EXIT trap behaves differently and is the right place for cleanup: it fires on normal exit, on exit caused by errexit, and after a handled signal, but it does not fire on SIGKILL. A subshell runs its own EXIT trap when it exits, which surprises people who put a cleanup that removes a shared temporary directory into a function called inside $(...).