Error handling inside addEventListener callback

Viewed 626

How do developers structure their programs if they want to have a top-level error handling function?

The immediate thought that came into my mind was to wrap a try..catch to the main function, however, this does not trigger errors from callbacks?

try {
  main();
} catch(error) {
  alert(error)
}

function main() {
  
  // This works
  throw new Error('Error from main()');
  
  document.querySelector('button').addEventListener('click', function() {
   // This doesn throw
   throw new Error ('Error from click callback');
  })
  
}
<button>
  Click me to see my callback error
</button>

2 Answers

Try-catch functionality around already existing functions/methods gets achieved best by wrapper approaches.

For the OP's use case one needs a modifying wrapper function which explicitly targets the handling of "after throwing" ...

// - try-catch wrapper which specifically
//   targets the handling of "after throwing".
function afterThrowingModifier(proceed, handler, target) {
  return function (...argsArray) {
    let result;
    try {
      result = proceed.apply(target, argsArray);
    } catch (exception) {
      result = handler.call(target, exception, argsArray);
    }
    return result;
  }
}

function failingClickHandler(/* event */) {
  throw new Error('Error from click callback');
}
function afterTrowingHandler(error, [ event ]) {
  const { message, stack } = error
  const { type, currentTarget } = event;
  console.log({
    error: { message, stack },
    event: { type, currentTarget },
  });
}

function main() {
  document
    .querySelector('button')
    .addEventListener('click', afterThrowingModifier(
      failingClickHandler, afterTrowingHandler
    ));
}
main();
body { margin: 0; }
.as-console-wrapper { min-height: 85%!important; }
<button>
  Click me to see my callback error
</button>

One of cause can implement prototype based abstractions for a function modifying failure handling like afterThrowing or afterFinally. Then the above main example code changes to something more expressive like ...

function afterTrowingHandler(error, [ event ]) {
  const { message, stack } = error
  const { type, currentTarget } = event;
  console.log({
    error: { message, stack },
    event: { type, currentTarget },
  });
}

function main() {
  document
    .querySelector('button')
    .addEventListener('click', (function (/* event */) {

      throw new Error('Error from click callback');

    }).afterThrowing(afterTrowingHandler));
}
main();
body { margin: 0; }
.as-console-wrapper { min-height: 85%!important; }
<button>
  Click me to see my callback error
</button>

<script>
  (function (Function) {

    function isFunction(value) {
      return (
        typeof value === 'function' &&
        typeof value.call === 'function' &&
        typeof value.apply === 'function'
      );
    }
    function getSanitizedTarget(value) {
      return value ?? null;
    }

    function afterThrowing/*Modifier*/(handler, target) {
      target = getSanitizedTarget(target);

      const proceed = this;
      return (

        isFunction(handler) &&
        isFunction(proceed) &&

        function afterThrowingType(...argumentArray) {
          const context = getSanitizedTarget(this) ?? target;

          let result;
          try {
            // try the invocation of the original function.

            result = proceed.apply(context, argumentArray);

          } catch (exception) {

            result = handler.call(context, exception, argumentArray);
          }
          return result;
        }

      ) || proceed;
    }
    // afterThrowing.toString = () => 'afterThrowing() { [native code] }';

    Object.defineProperty(Function.prototype, 'afterThrowing', {
      configurable: true,
      writable: true,
      value: afterThrowing/*Modifier*/
    });

  }(Function));
</script>

In javascript you can override global onerror, catching most of the errors:

window.onerror = function(message, source, lineno, colno, error) { ... };

https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror

In your case:

    window.onerror = function(message, source, lineno, colno, error) { 
        console.error(message);
        alert(message);
        return false
    };
    
    function main() {
      
      // This works
      throw new Error('Error from main()');
      
      document.querySelector('button').addEventListener('click', function() {
       // This doesn throw
       throw new Error ('Error from click callback');
      })
    }
    
    main();

Some extra info: https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror

Added after questions if Promise would raise the error, lets test:

window.onerror = (message, source, lineno,colno,error)=>{
    console.error(`It does!, ${message}`);
};
const aFn = ()=>{
    return new Promise((resolve)=>{
        setTimeout(()=>{
            throw new Error("whoops")
        }, 3000);
    });
}
aFn();

Result:

VM1163:2 It does!, Script error.
window.onerror @ VM1163:2
error (asynchroon)
(anoniem) @ VM1163:1
VM1163:7 Uncaught Error: whoops
    at <anonymous>:7:19
Related