For other translations of this series, see [JS Working Mechanism – Xiaobai’s 1991 column – Nuggets (juejin. Cn)] (juejin.cn/column/6988…

Exception handling is very common and important in front-end engineering. But not many people take it seriously. Full reading is recommended.

An overview of the

Anomaly detection is a technical means to ensure the program can run reliably.

One approach to anomaly detection is anomaly checking. Exception checking maintains normal program flow and checks for exceptions, which are reported using special return values, auxiliary global variables, or floating point status values.

Exceptions occur during program execution and interrupt normal flow. Such an interrupt triggers a predefined exception handler.

Note that exceptions can occur on both hardware and software.

Exception in JS

A JS application can run on a variety of operating systems, browsers or hardware devices. No matter how many tests you write, in such a complex environment, there will always be exceptions. From the end user’s point of view, JS handles exceptions silently. But the mechanics behind it are a little more complicated. As soon as part of the code fails, JS will throw an exception. Instead of continuing to execute the code, the JS engine checks to see if there is a handle function for exception handling.

If there is no exception handle, the engine will return and throw an exception. This process is then repeated for each function in the call stack until a handle to handle the exception is found. If no handle is found and no function is left on the stack, the Event loop adds the next function in the callback queue to the stack.

When an exception occurs, an Error object is generated and the exception is thrown.

The type of the Error object

JS has nine built-in exception objects that are fundamental to exception handling:

  • Error – Represents a generic exception, often used to implement user-defined exceptions.
  • EvalError– Not used correctlyeval()Delta function
  • RangeError – Occurs when a numeric variable or parameter is accessed beyond its possible range
  • ReferenceError – Occurs when accessing a variable that does not exist
  • SyntaxError – Occurs when JS syntax rules are not followed. For static languages, this error is triggered at compile time. In the case of JS it is triggered at execution time.
  • TypeError – Occurs when a value does not match the expected type. This exception is also raised by calling a nonexistent object method
  • URIError– callencodeURI() 和 decodeURI()An invalid URL was encountered
  • AggregateError– Multiple exceptions need to be merged into a single report, for examplePromise.any()
  • InternalError – An exception thrown internally by the JS engine. For example, “too much recursion”, the API is not standardized yet.

By inheriting built-in exceptions, you can also customize the types of exceptions.

An exception is thrown

JS allows developers to invoke throws to raise exceptions.

if (denominator === 0) {
    throw new RangeError("Attempted division by zero");
}
Copy the code

Each built-in exception object has an optional ‘message’ parameter, which makes the exception description more readable.

You can throw any type of exception, such as numbers, strings, arrays, etc

throw true; throw 113; Throw 'error message'; throw null; throw undefined; throw {x: 1}; Throw new SyntaxError(' hard to debug ');Copy the code

These are valid JS declarations.

Use built-in exception types over other objects because browsers take special care of them, such as the filename that caused the exception, the number of lines, the call stack trace, etc. Some browsers, such as Firefox, collect these properties for all types of exception objects

Handle exceptions

Now let’s see how we can ensure that exceptions don’t destroy our application.

“Try” statement

Like other programming languages, JS has try, catch, finally declarations that allow us to control the flow of exceptions.

Such as:

try {
    // a function that potentially throws an error
    someFunction();
} catch (err) {
    // this code handles exceptions
    console.log(e.message);
} finally {
    // this code will always be executed
    console.log(finally’);
}
Copy the code

The try statement enforces a block of code that might throw an exception.

“Catch” statement

This is followed by the “catch” statement, which wraps around the exception handling block. The “catch” statement keeps the exception from proliferating and allows the program to continue executing. The exception itself is passed as an argument to the catch statement.

Some blocks of code can throw different types of exceptions, and your application can support a variety of exceptions. The instanceof operation can be used to distinguish between different types of exceptions

try { If (typeof x ! == 'number') {throw new TypeError(' x is not a number '); } else if (x <= 0) {throw new RangeError(' x should be greater than 0 '); } else { // Do something useful } } catch (err) { if (err instanceof TypeError) { // Handle TypeError exceptions } else if (err instanceof RangeError) { // Handle RangeError exceptions } else { // Handle all other types of exceptions } }Copy the code

This example rethrows a caught exception. For example, if you catch an exception, but it is not relevant to your context, you can throw it again. ,

“Finally” statement

A finally block of code executes after a try and catch, ignoring any exceptions (in other words, if an exception occurs, then finally must be executed). The finally statement can be used to perform some cleanup, such as closing WebSocket connections.

The finally block executes even if the exception is not caught. In this scenario, after the finally block is executed, the engine continues to check the functions in the call stack in sequence until the correct exception handle is found or until the application is shut down.

Also, finally is executed even if a try or catch has already executed a return.

Look at an example:

function foo1() { try { return true; } finally { return false; }}Copy the code

The foo1() function returns false, even if the try already has a return declaration.

Here is an example of the same result:

function foo2() { try { throw new Error(); } catch { return true; } finally { return false; }}Copy the code

The function foo1() returns false

Handle exceptions in asynchronous code

We discussed the mechanics of asynchronous programming in JS earlier. Here we will see how to handle exceptions in “callback functions”, “promises”, and “async/await”.

async/await

Define a standard function that throws an exception

 async function foo() {
     throw new Error();
 }
Copy the code

When an exception is thrown in an async function, a ‘Rejected’ promise is returned along with the exception thrown

return Promise.Reject(new Error())
Copy the code

See what happens when foo() is called

try { foo(); } catch(err) {// This block won't be reached.} finally {// This block will be reached before the Promise is rejected. }Copy the code

Since foo() is asynchronous, it issues a Promise. The code does not wait for the async function to finish, so no exception is actually caught at this point. The finally block executes and returns a Promise and Rejected. We don’t have any code to handle this Rejected Promise. You can handle the promise by adding an await keyword when calling foo() and including the code with an async function.

async function run() {
    try {
        await foo();
    } catch(err) {
        // This block will be reached now.
    } finally {
        // This block will be reached at the end.
    }
}
 run();
Copy the code

Promises

Throw an exception outside the Promise with a function

function foo(x) { if (typeof x ! == 'number') { throw new TypeError('x is not a number'); } return new Promise((resolve, reject) => { resolve(x); }); }Copy the code

Now pass foo a string instead of a number

Foo (' test ').then(x => console.log(x)).catch(err => console.log(err));Copy the code

This raises Uncaught TypeError: x is not a number because the Promise catch cannot handle the exception yet — it is thrown outside the promise

This exception can be caught using standard try and catch statements

Try {foo(' test ').then(x => console.log(x)).catch(err => console.log(err)); } catch(err) { // Now the error is handed }Copy the code

If you modify Foo, throw an exception inside the Promise

function foo(x) { return new Promise((resolve, reject) => { if (typeof x ! == 'number') { throw new TypeError('x is not a number'); } resolve(x); }); }Copy the code

Now the catch statement handles the exception

Try {foo(' test ').then(x => console.log(x)).catch(err => console.log(err)); // The error is handled here. } catch(err) { // This block is not reached since the thrown error is inside of a Promise. }Copy the code

Note that throwing an exception in a Promise is the same as using a Reject callback. So we can define foo this way

function foo(x) { return new Promise((resolve, reject) => { if (typeof x ! == 'number') { reject('x is not a number'); } resolve(x); }); }Copy the code

If there is no catch method to handle the exception inside the Promise, the next function in the callback queue is added to the call stack.

Callback Functions

There are two main principles for using an exception-first callback strategy:

  1. The first argument to the callback is the error object, which is set to if an exception occurserrAnd then back. If there are no exceptions,errnull
  2. The second argument to the callback is the data that comes back
function asyncFoo(x, callback) { // Some async code... } asyncFoo(' testParam ', (err, result) => {If (err) {// Handle error.} // Do some other work.});Copy the code

If you have an Err object, it is best not to touch or rely on the Result parameter.

What about unhandled exceptions

If you use a third party library, you have no permission to handle exceptions. Look at the following example when you want to handle exceptions that have no handle

The browser

The window.onError event in the browser can handle this: Example:

window.onerror = (msg, url, line, column, err) => { // ... Handle the error... return false; };Copy the code

Its parameters look like this:

  • msg– Exception association information, for exampleUncaught ReferenceError: foo is not defined
  • Url – The URL of the script or document associated with this exception
  • LineNo – Number of lines of code (if any)
  • ColumnNo – Number of code columns (if any)
  • Err – Exception object (if any).

If a function returns true, it prevents the default event handle from firing.

Only one event handle can be assigned to window.onError at a time. This means that if you want to assign, you need to overwrite handles that have been written by third-party libraries before. This can cause big problems, especially for tools like exception tracking, which can shut down completely. Use the following tips to solve this problem

var oldOnErrorHandler = window.onerror;
window.onerror = (msg, url, line, column, err) => {
    If (oldOnErrorHandler) {
        // Call any previously assigned handler.   
	oldOnErrorHandler.apply(this, arguments);
    }

    // The rest of your code
}
Copy the code

This code first checks to see if window.onError has been defined before and then simply calls it before processing. Using this method, you can add handles to window.onError as you like

This approach works in all browsers.

Another way to do this without replacing handles is to add event listeners to the Window object

window.addEventListener('error', e => { 
    // Get the error properties from the error event object 
    const { message, filename, lineno, colno, error } = e; 
});
Copy the code

This approach is better and more widely supported

Node.js

The process object in the EventEmmiter module provides two events to handle exceptions:

  1. uncaughtException— Occurs when an uncaught exception bubbles in the [event loop]. By default, Node.js prints the stack trace of the exception to stderr, then exits and returnscode 1. You can add a handle to this event. The proper way to use this event is to perform an asynchronous resource cleanup (such as file descriptors, handles, and so on) before the thread closes. It is not safe to perform normal operations after this.
  2. unhandledRejection– whenPromiseTriggered when rejected is rejected and there is no exception handling. When probing and tracing a promise rejected with no exception handle,unhandledRejectionIs very useful.
process
    .on('unhandledRejection', (reason, promise) => {
         // Handle failed Promise
    })
    .on('uncaughtException', err => {
        // Handle failed Error   
        process.exit(1);
     });
Copy the code

It is important to handle exceptions correctly in your code, and only by understanding unhandled exceptions can you handle them properly.

You can do it yourself, but it can be a bit cumbersome, and you need to consider different scenarios for different browsers. You can also use third-party tools to do this. Either way, you need to understand as much as possible about exceptions and the context in which they are triggered so that you can easily reproduce them.