0

This is just another hopeless try to handle errors in async event handlers.

A note about this example: The example here works differently than it does if it is run directly in the browser. If ran directly in the browser none of the event listeners for errors is working ("error", "unhandledrejection").

It looks similar on Windows 10 in Chrome (Version 80.0.3987.163 (Official Build) (64-bit)) and Firefox (75.0 (64-bit)).

The only way I have found to handle this is to never make any typos. But that does not work either for me.

How is this supposed to work?

window.addEventListener("error", evt => {
    console.warn("error event handler", evt);
    output("error handler: " + evt.message, "yellow");
});
window.addEventListener("unhandledrejection", evt => {
    console.warn("rejection event handler", evt);
    output("rejection handler: " + evt.message, "green");
});
function output(txt, color) {
    const div = document.createElement("p");
    div.textContent = txt;
    if (color) div.style.backgroundColor = color;
    document.body.appendChild(div);
}

const btn = document.createElement("button");
btn.innerHTML = "The button";
btn.addEventListener("click", async evt => {
    evt.stopPropagation();
        output("The button was clicked");
        noFunction(); // FIXME: 
})
document.body.appendChild(btn);

const btn2 = document.createElement("button");
btn2.innerHTML = "With try/catch";
btn2.addEventListener("click", async evt => {
    evt.stopPropagation();
    try {
        output("Button 2 was clicked");
        noFunction2(); // FIXME: 
    } catch (err) {
        console.warn("catch", err)
        throw Error(err);
    }
})
document.body.appendChild(btn2);

new Promise(function(resolve, reject) {
    setTimeout(function() {
        return reject('oh noes');
    }, 100);
});

justAnError();
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1">
<script defer src="error-test.js"></script>


EDIT - adding output from Chrome and JS Bin (Link to JS Bin example)

Loading page

Chrome/Firefox:

error handler: Script error.

JS Bin:

error handler: Uncaught ReferenceError: justAnError is not defined

rejection handler: undefined

Clicking left button

Chrome/Firefox:

The button was clicked

JS Bin:

The button was clicked

rejection handler: undefined

16
  • 2
    "If ran directly in the browser none of the event listeners for errors is working" They do for me (Chrome, Firefox), locally and in that JSBin. Please post a complete HTML page replicating the problem, exactly as you're authoring it locally. Commented Apr 7, 2020 at 13:17
  • 2
    Note: I strongly recommend not using async functions directly as event handlers. The event system doesn't do anything with the promise they return, which is why errors in them end up being unhandled rejections. If you want to centralize error handling for them, one option would be to have a wrapper function: addEventListener("event", handler(async evt => { /*...*/ }). The handler function would call the async function and handle any rejections using your centralized means of doing so. Commented Apr 7, 2020 at 13:19
  • 1
    "The only way I have found to handle this is to never make any typos" - uh, these event handlers cannot handle typos in your code either. At best, they can notify you about them. In development, the console can do that as well, in production, the best solution is not to roll out wrong code :-) Commented Apr 7, 2020 at 13:20
  • 1
    @Leo - Please be more specific about what you mean by "does not work." I get the same results from the snippet, JSBin, and local. "So handler is a function taking a promise as an argument?" No, it takes an async function as an argument, and returns a function to use as the event handler; that function handles rejections: function handler(fn) { return function(evt) { fn(evt).catch(e => { /*...handle rejection in `e`... */}); }; } Commented Apr 7, 2020 at 15:26
  • 1
    @T.J.Crowder Ah, yes. I was just trying to implement the suggestion and realized what you mean. ;-) Commented Apr 7, 2020 at 15:34

1 Answer 1

1

You could give yourself utility functions for error reporting and wrapping event handlers, like this:

function handleError(err) {
    if (!(err instanceof Error)) {
        err = Error(err);
    }
    output("error handler: " + err.message, "yellow");
}

function wrapHandler(fn) {
    return function(evt) {
        new Promise(resolve => {
            resolve(fn(evt));
        }).catch(e => {
            handleError(e);
        });
    };
}

That supports both async and non-async event handlers. If there's a synchronous error calling fn, it's caught by the promise constructor and turned into a rejection of the promise being created. If there isn't, the promise is resolved to the return value of the fn, meaning that if fn returns a promise that rejects, the promise created by new Promise is rejected. So either way, errors go to the error handler.

I haven't tried to distinguish between errors and rejections, as they're fundamentally the same thing, but you could if you want:

function handleError(err, isRejection) {
    if (!(err instanceof Error)) {
        err = Error(err);
    }
    output("error handler: " + err.message, isRejection ? "green" : "yellow");
}

function wrapHandler(fn) {
    return function(evt) {
        try {
            const result = fn(event);
            Promise.resolve(result).catch(e => handleError(e, true));
        } catch (e) {
            handleError(e, false);
        }
    };
}

Either way, you'd set up your global handlers to use it and prevent the default:

window.addEventListener("error", errorEvent => {
    handleError(errorEvent.error, false); // Remove the `, false` if you're not trying to make a distinction
    errorEvent.preventDefault();
});

window.addEventListener("unhandledrejection", errorEvent => {
    handleError(errorEvent.reason, true); // Remove the `, true` if you're not trying to make a distinction
    errorEvent.preventDefault();
});

You'd use wrapHandler when setting up your handlers, either directly:

btn.addEventListener("click", wrapHandler(async evt => {
    evt.stopPropagation();
    output("The button was clicked");
    noFunction(); // FIXME: 
}));

...or by having another utility function:

function addListener(elm, eventName, fn) {
    const handler = wrapHandler(fn);
    return elm.addEventListener(eventName, handler);
    return function() {
        elm.removeEventListener(handler);
    };
}

...then:

const removeBtnClick = addListener(btn, "click", async evt => {
    evt.stopPropagation();
    output("The button was clicked");
    noFunction(); // FIXME: 
});
// ...if you want to remove it later...
removeBtnClick();

Live Example — since your original distinguished between synchronous errors and rejections, I've used that variant here, but again, its' really a distinction without a difference and I wouldn't distinguish them in my own code:

function handleError(err, isRejection) {
    if (!(err instanceof Error)) {
        err = Error(err);
    }
    output("error handler: " + err.message, isRejection ? "green" : "yellow");
}

window.addEventListener("error", errorEvent => {
    handleError(errorEvent.error, false);
    errorEvent.preventDefault();
});

window.addEventListener("unhandledrejection", errorEvent => {
    handleError(errorEvent.reason, true);
    errorEvent.preventDefault();
});

function wrapHandler(fn) {
    return function(evt) {
        try {
            const result = fn(event);
            Promise.resolve(result).catch(e => handleError(e, true));
        } catch (e) {
            handleError(e, false);
        }
    };
}

function addListener(elm, eventName, fn) {
    const handler = wrapHandler(fn);
    return elm.addEventListener(eventName, handler);
    return function() {
        elm.removeEventListener(handler);
    };
}

function output(txt, color) {
    const div = document.createElement("p");
    div.textContent = txt;
    if (color) div.style.backgroundColor = color;
    document.body.appendChild(div);
}

const btn = document.createElement("button");
btn.innerHTML = "The button";
addListener(btn, "click", async evt => {
    evt.stopPropagation();
    output("The button was clicked");
    noFunction(); // FIXME: 
});
document.body.appendChild(btn);

const btn2 = document.createElement("button");
btn2.innerHTML = "With try/catch";
addListener(btn2, "click", async evt => {
    evt.stopPropagation();
    try {
        output("Button 2 was clicked");
        noFunction2(); // FIXME: 
    } catch (err) {
        console.warn("catch", err)
        throw Error(err);
    }
});
document.body.appendChild(btn2);

new Promise(function(resolve, reject) {
    setTimeout(function() {
        return reject('oh noes');
    }, 100);
});

justAnError();
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1">

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, that was very nice.
Just a clarification: Your solution works for both async and non-async event handlers. But as I understand it it is not needed in the non-async case.
@Leo - Well, it provides nice centralized error handling even for non-async handlers. :-)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.