1

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>
Toxnyc
  • 1,350
  • 7
  • 20

2 Answers2

2

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>
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
1

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
Firewizz
  • 773
  • 5
  • 17
  • That's interesting! Does this catch promise-based errors? e.g if the main() was an async function. – Toxnyc Nov 05 '21 at 16:48
  • @Toxnyc ... most probably not since one basic trait of promises is the early exit of a (chain) of promise(s) and the handling (thus before catching) of an exception/error/reason. Of cause, the top most exception handler can be implemented to finally throw again. But what should it be good for? – Peter Seliger Nov 05 '21 at 17:59
  • @PeterSeliger This was very simple to test, so I updated the question with the test. But the short answer, it does. reason is that Javascript calls the window.onerror on every error. – Firewizz Nov 07 '21 at 16:19
  • @PeterSeliger To answer what is it good for to catch top level, this could be logging. We use it all the time for logging purposes. For correct error handling, you use try...catch in the promise and return the reject. But as far as I understood, this was not the question. – Firewizz Nov 07 '21 at 16:29
  • The topic states ... _"Error handling **inside** addEventListener callback"_. And since this is not possible if not implemented originally, the technique/approach usually used is **wrapping**. My answer does focus exactly on this one, since you already did show another possible approach of handling it globally. – Peter Seliger Nov 07 '21 at 16:47
  • @Toxnyc for promise-based errors, there's the [`unhandledrejection` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event) in addition to the `error` event. But either of these is only a method of last resort, to collect errors for reports and statistics, not to handle them. – Bergi Nov 07 '21 at 16:56
  • @PeterSeliger, I can find myself in your approach. Tho, what I read is "...if they want to have a top-level error handling". But indeed, if you want to catch inside the listener... But even then, I suggest writing proper error handling inside the method there. – Firewizz Nov 08 '21 at 08:29
  • 1
    Thank you all for contributing to the discussion, they are really helpful! I realized that the title of my post was a bit misleading. I was looking for a way to catch ALL errors(simply displaying them to the user), addEventListener and promise-based func were just examples. FWIW, window.onerror is closest to what I needed, so I will accept this as the answer. – Toxnyc Nov 10 '21 at 03:29