Regardless of your skill level, errors or exceptions are part of your application developer’s life. The inconsistency of web development leaves many places where errors can and do have happened. The key to the solution is to deal with any unforeseen (or foreseeable errors) to control the user's experience. With JavaScript, there are a variety of technical and language features that can be used to correctly solve any problem.
It is dangerous to handle errors in JavaScript. If you believe in Murphy's Law, what will go wrong will eventually go wrong! In this article, I will dig into error handling in JavaScript. I will involve some pitfalls and good practices. Finally, we will discuss asynchronous code processing and Ajax.
I think the event-driven model of JavaScript adds rich meaning to this language. I think there is no difference between the event-driven engine and the error-reporting mechanism of this kind of browser. Whenever an error occurs, it is equivalent to throwing an event at a certain point in time. In theory, we can handle error throwing events in JavaScript like we handle ordinary events. If this sounds strange to you, focus on starting the journey below. This article only targets the client's JavaScript.
Example
The code examples used in this article can be obtained on GitHub. The current page looks like this:
Clicking each button will throw an error. It simulates the exception of the TypeError type. The following is the definition and unit test of such a module.
function error() { var foo = {}; return foo.bar();}First, this function defines an empty object foo. Note that the bar() method is not defined anywhere. We use unit tests to verify that this does throw an error.
it('throws a TypeError', function () { should.throws(target, TypeError);});This unit test uses test assertions from the Mocha and Should.js libraries. Mocha is a running test framework, and should.js is an assertion library. If you are not very familiar with them, you can browse their documents online for free. A test case usually starts with it('description') and ends with passing or failure of the assertion in should. The advantage of using this framework is that it can be unit tested in node, rather than in the browser. I suggest you take these tests seriously because they validate many key basic concepts in JavaScript.
As shown above, error() defines an empty object and then tries to call the method in it. Because the bar() method does not exist in this object, it will throw an exception. Trust me, in dynamic languages like JavaScript, anyone can make such mistakes.
Bad demonstration
Let’s take a look at the poor error handling methods. I'm going to abstract the wrong action and bind it to the button. Here is what the unit test of the handler looks like:
function badHandler(fn) { try { return fn(); } catch (e) { } return null;}This processing function receives a callback function fn as a dependency. Then the function is called inside the handler. This unit test examples how to use this method.
it('returns a value without errors', function() { var fn = function() { return 1; }; var result = target(fn); result. should.equal(1);});it('returns a null with errors', function() { var fn = function() { throw Error('random error'); }; var result = target(fn); should(result).equal(null);});As you can see, if an error occurs, this weird way of handling returns a null. This callback function fn() will point to a legal method or error. The click processing event below completes the rest.
(function (handler, bomb) { var badButton = document.getElementById('bad'); if (badButton) { badButton.addEventListener('click', function () { handler(bomb); console.log('Imagine, getting promoted for hiding mistakes'); }); }}(badHandler, error));The bad thing is that what I just got was a null. This made me very confused when I was thinking about determining what was wrong. This strategy of silence when errors occur covers every link from user experience design to data corruption. The frustrating side followed was that I had to spend hours debugging but couldn't see the error in the try-catch code block. This weird processing hides all errors in the code, and it assumes that everything is normal. This can be executed smoothly in some teams that do not pay attention to code quality. However, these hidden errors will eventually force you to spend hours debugging your code. In a multi-layer solution that relies on the call stack, it is possible to determine where the error comes from. It may be appropriate to silently handle the try-catch in rare cases. But if you encounter an error, you will deal with it, which is not a good solution.
This failure-that is silence strategy will prompt you to better deal with errors in your code. JavaScript provides a more elegant way to deal with this type of problem.
Not readable solution
Let's continue, let's take a look at the difficult-to-understand approach. I'll skip the tightly coupled part with the DOM. This part is no different from the bad approach we just saw. The focus is on the part of the unit test below that handles exceptions.
function uglyHandler(fn) { try { return fn(); } catch (e) { throw Error('a new error'); }}it('returns a new error with errors', function () { var fn = function () { throw new TypeError('type error'); }; should.throws(function () { target(fn); }, Error);});There is a good improvement compared to the bad treatment just now. The exception is thrown in the call stack. What I like is that the errors are freed from the stack, which is hugely helpful for debugging. When an exception is thrown, the interpreter will look at the next processing function at a level in the call stack. This provides many opportunities to handle errors at the top level of the call stack. Unfortunately, because he is a difficult mistake, I can't see the original error information. So I have to look along the call stack and find the most primitive exception. But at least I know there is an error happening where the exception is thrown.
Although this unreadable error handling is innocuous, it makes the code difficult to understand. Let's see how the browser handles errors.
Call stack
Then, one way to throw an exception is to add a try...catch code block at the top level of the call stack. For example:
function main(bomb) { try { bomb(); } catch (e) { // Handle all the error things }}But, remember I said that browsers are event-driven? Yes, an exception in JavaScript is nothing more than an event. The interpreter stops the program at the context where the exception is currently occurring and throws an exception. To confirm this, the following is a global event handling function onerror that we can see. It looks like this:
window.addEventListener('error', function (e) { var error = e.error; console.log(error);});This event handler catches errors in the execution environment. Error events will produce various errors in various places. The point of this approach is to centrally handle errors in the code. Just like other events, you can use a global handler to handle various errors. This makes error handling only a single goal if you adhere to the SOLID (single responsibility, open-closed, Liskov substitution, interface segregation and dependency inversion) principles. You can register error handling functions at any time. The interpreter executes these functions loops. The code is freed from statements filled with try...catch and becomes easy to debug. The key to this approach is to handle errors that occur just like normal JavaScript events.
Now, there is a way to display the call stack with a global processing function. What can we do with it? After all, we have to use the call stack.
Record the call stack
The call stack is very useful in handling bug fixes. The good news is that the browser provides this information. Even though the stack attribute of the error object is not standard at present, this attribute is generally supported in newer browsers.
So, the cool thing we can do is print it out to the server:
window.addEventListener('error', function (e) { var stack = e.error.stack; var message = e.error.toString(); if (stack) { message += '/n' + stack; } var xhr = new XMLHttpRequest(); xhr.open('POST', '/log', true); xhr.send(message);});It may not be obvious in the code example, but this event handler will be triggered by the previous error code. As mentioned above, each handler has a single purpose, which makes the code DRY(don't repeat yourself without repeatedly making the wheel). What I'm interested in is how to capture these messages on the server.
Here is a screenshot of the node runtime:
The call stack is very helpful for debugging the code. Never underestimate the role of the call stack.
Asynchronous processing
Oh, it's quite dangerous to handle asynchronous code! JavaScript brings asynchronous code out of the current execution environment. This means there is a problem with the try...catch statement below.
function asyncHandler(fn) { try { setTimeout(function () { fn(); }, 1); } catch (e) { }}There is still some remaining part of this unit test:
it('does not catch exceptions with errors', function () { var fn = function () { throw new TypeError('type error'); }; failedPromise(function() { target(fn); }). should.be.rejectedWith(TypeError);});function failedPromise(fn) { return new Promise(function(resolve, reject) { reject(fn); });}I have to end this handler with a promise to verify the exception. Note that although my code is all in try...catch, an unhandled exception still appears. Yes, try...catch only works in a separate execution environment. When the exception is thrown, the interpreter's execution environment is no longer the current try-catch block. This behavior occurs similarly to Ajax calls. So, now there are two options. An alternative is to catch exceptions in an asynchronous callback:
setTimeout(function () { try { fn(); } catch (e) { // Handle this async error }}, 1);Although this method is useful, there is still a lot of room for improvement. First, try...catch code blocks appear everywhere in the code. In fact, in the 1970s programming calls, they wanted their code to fall back. In addition, the V8 engine discourages the use of try…catch code blocks in functions (V8 is the JavaScript engine used by Chrome browser and Node). They recommend writing these blocks of code that catches exceptions at the top of the call stack.
So, what does this tell us? As I said above, global error handlers in any execution context are necessary. If you add an error handler to a window object, that is, you're done! Isn't it nice to follow the principles of DRY and SOLID? A global error handler will keep your code readable and clean.
The following is the report printed on the server-side exception handling. Note that if you use the code in the example, the output may vary slightly depending on the browser you are using.
This handler can even tell me which error comes from asynchronous code. It tells me that the error comes from the setTimeout() handler. So cool!
Errors are part of every application, but proper error handling is not. There are at least two ways to deal with errors. One is a solution of failure, silence, that is, ignore errors in the code. Another way is to quickly detect and resolve errors, i.e. stop at the error and reproduce. I think I have expressed clearly what I like and why I like it. My Choice: Don't hide the problem. No one will blame you for unexpected events in your program. This is acceptable, to break points, reproduce, and give the user a try. In an imperfect world, it is important to give yourself a chance. Errors are inevitable, and what you do is important to solve the error. Reasonable use of JavaScript's error handling features and automatic and flexible decoding can make the user's experience smoother, and also make the developer's diagnosis work easier.