preface

There are several reasons why front-end exceptions are handled:

  1. Improve code robustness: It is important for developers that the more robust the code, the less likely the system is to break;
  2. Improve system stability: Exceptions may cause problems such as failure of normal processes, page style disorder, crash and even blank screen, which may cause service loss.
  3. Enhance user experience: code errors should not affect the normal display of the page and user interaction. When errors occur, we need to use a drag-bottom solution or give feedback to the user.
  4. Locating faults: Only when we know how to handle exceptions, we can report them to the front-end monitoring system to discover and locate faults in time.

This paper is divided into the following three parts: The first part: introduces the Error object and Error type; The second part introduces how to catch exceptions, including common, Vue and React projects, catch in iframe and page crash exceptions. The third part: combined with the work of the scene, summed up their corresponding exception handling methods.

I’ve tried to provide examples for the first two parts of this article, which I hope you found useful. In addition, because this article is to do summary use, will be quite long, you can according to their own needs to see the corresponding part.

Error and the Error type

When it comes to exceptions, we need to start with the Error object. When JavaScript is running, if an Error occurs, the browser throws an instance object of the Error.

The Error object

Error is the JavaScript Error class, which is also a constructor that can be used to create an Error object. Create an Error instance object as follows:

  new Error([message[, fileName[,lineNumber]]]);
Copy the code

In addition, Error can be used like a function, which returns an Error instance object if there is no new. So, just calling Error yields the same result as constructing the Error instance object with the new keyword.

The Error type

According to the MDN documentation, there are also the following Error types that inherit from Error objects:

  • SyntaxError
  • RangeError
  • ReferenceError
  • TypeError
  • URIError
  • EvalError
  • InternalError
  • AggregateError

I’ll explain what these types of errors mean in sequence and try to give examples.

  1. SyntaxError

A SyntaxError is an error that occurs when your code does not conform to Javascript syntax.

  // The variable name is incorrect
  let 1name // Uncaught SyntaxError: Invalid or unexpected token

  // The parentheses are missing
  console.log('test' // Uncaught SyntaxError: missing ) after argument list

  // The string is not quoted
  a string // Uncaught SyntaxError: Unexpected identifier
Copy the code
  1. RangeError

A RangeError is an error when a value is not in the allowed range or set.

  // Pass an invalid length value as an argument to the Array constructor to create an Array
  new Array(-1) // Uncaught RangeError: Invalid array length

  // Pass the error value to the numerical calculation method
  var number = 10
  number.toFixed(-1) // Uncaught RangeError: toFixed() digits argument must be between 0 and 100
Copy the code
  1. ReferenceError

ReferenceError is an error that occurs when referring to a variable that does not exist or assigning a value to an object that cannot be assigned.

  // The variable name is undefined
  undefinedVariable // Uncaught ReferenceError: unknowName is not defined

  // Method name is undefined
  undefinedFunction() // Uncaught ReferenceError: undefinedFunction is not defined

  // the left side of the equals sign cannot be assigned //todo: why
  console.log() = 1 // Uncaught SyntaxError: Invalid left-hand side in assignment

  // the left side of the equals sign cannot be assigned //todo: why
  if(a === 1 || b = 2) {
      console.log('a === 1 || b = 2')}// Invalid left-hand side in assignment

  // This object cannot be manually assigned
  this = 1 // Uncaught SyntaxError: Invalid left-hand side in assignment
Copy the code
  1. TypeError

TypeError is an error that occurs when the type of a variable or parameter is not the expected type.

  // The argument to the new command is not a constructor
  new 123 // Uncaught TypeError: 123 is not a constructor

  // The method used is not function
  let functionName = 'functionName'
  functionName() // Uncaught TypeError: functionName is not a function

  // undefined or null has no corresponding attribute or method
  undefined.value // Uncaught TypeError: Cannot read property 'value' of undefined
Copy the code
  1. URIError

URIError is an error thrown when arguments to urI-related functions are incorrect.

  decodeURI(The '%') // Uncaught URIError: URI malformed
Copy the code
  1. EvalError

EvalError represents an error that occurs when an eval function is not executed correctly. Note that this exception is no longer thrown by JavaScript, but the EvalError object remains compatible.

  // EvalError is not reported but SyntaxError corresponding to js execution
  eval('a string') // Uncaught SyntaxError: Unexpected identifier
Copy the code

Never use Eval! Eval () is a dangerous function that executes code with the same permissions as the caller. If the string code you run with Eval () is modified by a malicious party (someone with bad intentions), you could end up running malicious code on a user’s computer under the authority of your web page/extension. Eval () is generally slower than the alternatives because it must call the JS interpreter, while many other constructs can be optimized by modern JS engines.

  1. InternalError

InternalError represents an error that occurs inside the JavaScript engine.

Example scenarios are usually too large for some component, such as:

  • “Too many switch cases” (too many switch cases);
  • “Too many parentheses in regular expression”;
  • “Array initializer too large”;
  • “Too much recursion”.
  1. AggregateError

AggregateError is used to aggregate multiple errors. It is important to note that this is an experimental feature and is not yet supported by all browsers (promise.any used in the example below is also an experimental feature).

  Promise.any([
    Promise.reject(new Error("some error")))// Uncaught (in promise) AggregateError: All promises were rejected
Copy the code

Promise.any() receives a Promise iterable and returns the Promise that succeeded as soon as one of the promises succeeds. If none of the iterable promises succeeds (i.e. all Promises fail/reject), return an instance of the failed promise and AggregateError type.

We can also customize the exception type based on Error or throw any type of exception, but our goal in this article is to catch and handle exceptions thrown by the browser. We won’t say much about custom and manual throws here.

Catch exceptions

Now that we know what exceptions the browser throws, let’s take a closer look at what we can do at the code level to catch them and help improve the robustness of our code.

General way

try-catch

A try-catch statement marks the block of statements to try and specifies a response to throw if an exception occurs. A try statement contains a try block consisting of one or more statements. A catch clause contains the statement to be executed when an exception is thrown from the try block. If any of the statements (or functions called from the try block) in the try block throw an exception, control immediately switches to the catch clause. If no exception is thrown in the try block, the catch clause is skipped.

  try {
    const person = {};
    console.log(person.info.name);
  } catch (err) {
    console.log(err);
  }

  // TypeError: Cannot read property 'name' of undefined at <anonymous>:3:27
Copy the code

In the example above, we try to get the value of a property of undefined. The exception is caught by a catch and printed to the console.

Any given exception will only be caught once by the nearest closed catch block.

Sometimes we have try-catch nesting in our code. If there is no catch in the inner layer, the outer catch will catch it:

  try {
    try {
      throw new Error('error');
    }
    finally {
      console.log('finally'); }}catch (err) {
    console.log('outer', err);
  }

  // finally
  // VM1360:10 outer Error: error at <anonymous>:3:11
Copy the code

If the inner layer throws a new exception, the new exception is usually caught by the outer catch:

  try {
    try {
      throw new Error('error');
    }
    catch (err) {
      console.log('inner', err);
      throw err; // Throws a new exception that has not been caught by the inner layer}}catch (err) {
    console.log('outer', err);
  }

  // inner Error: error at <anonymous>:3:11
  // outer Error: error at <anonymous>:3:11
Copy the code

Try-catch is used when you know that a particular piece of code is likely to have a problem, and can only catch synchronous runtime errors, not syntactic and asynchronous errors:

  1. Syntax error: syntax error, try-catch not executed correctly.
  try {
    let 1a = 'a';
    console.log(1a);
  } catch (err) {
    console.log('catch syntax error');
  }

  // Uncaught SyntaxError: Invalid or unexpected token
Copy the code
  1. Asynchronous error: Cannot be caught because the asynchronous event is already in the asynchronous event queue.
  try {
    setTimeout(() = > {
      console.log(a)
    }, 1000);
  } catch (err) {
    console.log('catch async error');
  }

  // Uncaught ReferenceError: a is not defined at <anonymous>:3:17
Copy the code

GlobalEventHandlers.onerror

From GlobalEventHandlers. Onerror literal itself can be seen that the onerror to deal with the global error. Let’s take a look at the explanation on MDN:

The onError property of the hybrid event GlobalEventHandlers is used to handle the error event.

  • When JavaScript runtime errors (including syntax errors) occur, the window raises an error event from the ErrorEvent interface and executes window.onerror().
  • When a resource (image or JavaScript file) fails to load, the element that loaded the resource raises an error Event in the Event interface and executes the onError () handler on that element. These error events do not bubble up to Windows, but (at least in Firefox) can be caught by a single Window.addeventListener.

From the above text, we can draw the following conclusions:

  1. Window. onerror and window.addEventListener(‘error’, function(event) {… }) to capture;
  2. When static resource loading fails, an onError event will be raised on the element that loaded the resource. Since this event will not bubble to Winow, window.onerror will not catch the error of static resource loading failure.
  3. If you want to catch static resource load failures using a global method, you can use window.addeventListener.

To verify this, we define window.onerror and window.addEventListener (we need to write them at the beginning of all JavaScript scripts, otherwise we may not catch errors) :

  window.onerror = function(message, source, lineno, colno, error) {
    console.log('window.onerror catch error:', message);
  }
  window.addEventListener('error'.function(event) {
    console.log('window.addEventListener catch error:', event.message)
  });
Copy the code
  1. Grammar mistakes
  let 1a = 'a';
  console.log(1a);

  // window.onerror catch error: Uncaught SyntaxError: Invalid or unexpected token
  // window.addEventListener catch error: Uncaught SyntaxError: Invalid or unexpected token
Copy the code
  1. Static resource loading error

To catch static resource load failures, we can add an onError event to the static resource:

  <script src="https://misc.360buyimg.com/jdf/lib/jquery-1.6.4.000.js" onerror="console.log('script load onerror')"></script>

  // script load onerror
Copy the code

If we want to catch static resource loading errors globally, we need to add a third parameter to the addEventListener method, which sets useCapture to true:

  window.addEventListener('error'.function(event) {
    console.log('window.addEventListener catch error:', event.message)
  }, true); 
Copy the code

Loading an error JavaSctipt file:

  <script src="https://misc.360buyimg.com/jdf/lib/jquery-1.6.4.000.js"></script>

  // window.addEventListener catch error: < script SRC = "/ lib/HTTPS: / / misc.360buyimg.com/ JDF jquery - 1.6.4.000. Js" > < / script >
Copy the code
  1. Asynchronous error
  setTimeout(() = > {
    console.log(a)
  }, 1000);
  
  // window.onerror catch error: Uncaught ReferenceError: a is not defined
  // window.addEventListener catch error: Uncaught ReferenceError: a is not defined
Copy the code

Can be seen from the above example, GlobalEventHandlers onerror is suitable for the need to capture the global abnormal situation. In addition, window.onerror and window.addEventListener catch syntax errors and asynchronous errors in contrast to try-catch. Element. onError and window.addEventListener catch static resource load failures.

While window. onError and window.addEventListener can handle asynchronous errors, Promise’s asynchronous errors cannot be caught.

  new Promise((resolve, reject) = > {
      console.log(a)
  })

  // Uncaught (in promise) ReferenceError: a is not defined
Copy the code

promise-catch

Promise errors need to be caught using promise-catch, either when the code runs or when we process the business logic, reject.

  1. Code error
  new Promise((resolve, reject) = > {
      console.log(a)
  }).catch(err= > {
      console.log('promise catch error:', err.message)
  })

  // promise catch error: a is not defined
Copy the code
  1. Reject the mistake
  new Promise((resolve, reject) = > {
      reject(new Error('error rejected! '))
  }).catch(err= > {
      console.log('promise catch error:', err.message)
  })

  // promise catch error: error rejected!
Copy the code

The scope of a promise-catch is very clear, which is to handle exceptions for promises. The exception is that async/await, which is essentially a Promise syntax, can be caught by a try-catch. (Therefore we advocate using async/await instead of pure promises as it is easier to catch. If you still use promises, remember to add catch events or rely on global catching methods.)

  function fn() {
      return new Promise((resolve, reject) = > {
          console.log(a); resolve(); })}async function test() {
      try {
          await fn();
      } catch (err) {
          console.log('try-catch error:', err.message);
      }
  }
  test();

  // try-catch error: a is not defined
Copy the code

unhandledrejection

In our development, if some Promise exceptions were not handled, we could use a global approach to catch them, using the unhandledrejection event.

  window.onunhandledrejection = function(err) {
    console.log('window.onunhandledrejection catch error:', err.reason);
  }
  window.addEventListener('unhandledrejection'.function(event) {
    console.log('window.addEventListener unhandledrejection catch error:', event.reason);
  });

  // window.onunhandledrejection catch error: ReferenceError: a is not defined
  // window.addEventListener unhandledrejection catch error: ReferenceError: a is not defined
Copy the code

We use frameworks when we write front-end projects. In addition to the general method of catching exceptions above, the framework itself provides several methods for us to use.

Exceptions are caught in Vue

The official Vue documentation does not have a section devoted to exception handling. In general, there are several ways to do this in a production environment (development environment errors can be seen from the console, which is not covered here, see warnHandler and renderError on Vue’s website) :

  • errorHandler
  • errorCaptured

errorHandler

ErrorHandler is used in Vue to catch global errors:

  Vue.config.errorHandler = function (err, vm, info) {
    console.log('vue errorHandler: ' + err);
  }
Copy the code

The exceptions that errorHandler can catch include the following:

  1. Uncaught errors during rendering and viewing of components

Note that a reference to a variable in the template that does not exist will not be caught by an errorHandler. ErrorHandler is used to catch this error.

  <template>
      <div>{{currentTime}}</div>
  </template>

  <script>
  export default {
      name: 'ErrorTest',
      data () {
          return{}}}</script>

  // No exception was caught
Copy the code

Add currentTime variable to data, but assignment error:

  <template>
      <div>{{currentTime}}</div>
  </template>

  <script>
  export default {
      name: 'ErrorTest',
      data () {
          return {
              currentTime
          }
      }
  }
  </script>

  // vue errorHandler: ReferenceError: currentTime is not defined
Copy the code
  1. Catch errors in component lifecycle hooks (version >=2.2.0)
  <template>
      <div>{{currentTime}}</div>
  </template>

  <script>
  export default {
      name: 'ErrorTest',
      data () {
          return {
              currentTime: Date.now()
          }
      },
      mounted () {
          console.log(currentTime)
      }
  }
  </script>

  // vue errorHandler: ReferenceError: currentTime is not defined
Copy the code
  1. Error inside custom event handler (version >=2.4.0)

Let’s assume that the child component fires the change event using the $emit method:

  <template>
      <child @change="changeHandler" />
  </template>

  <script>
  import Child from './child'
  export default {
      name: 'ErrorTest'.components: {
          Child
      },
      methods: {
          changeHandler () {
              console.log(changedValue)
          }
      }
  }
  </script>

  // vue errorHandler: ReferenceError: changedValue is not defined
Copy the code
  1. Error thrown internally by V-ON DOM listener (version >=2.6.0)
  <template>
      <button v-on:click="clickHandler">click here</button>
  </template>

  <script>
  export default {
      name: 'ErrorTest'.methods: {
          clickHandler () {
              console.log(target)
          }
      }
  }
  </script>

  // vue errorHandler: ReferenceError: target is not defined
Copy the code
  1. If any overridden hook or handler returns a Promise chain (such as async function), errors from its Promise chain are also handled. (version > = server)
  <template>
      <button v-on:click="clickHandler">click here</button>
  </template>

  <script>
  export default {
      name: 'ErrorTest'.methods: {
          clickHandler () {
              return new Promise(() = > {
                  console.log(target)
              }) // You must return, otherwise you will not be caught}}}</script>

  // vue errorHandler: ReferenceError: target is not defined
Copy the code

errorCaptured

ErrorCaptured is a hook function added to Vue in 2.5.0 for catching errors from child components. For now, we still assume that the child component threw an error (the errorHandler method mentioned in the previous section remains) :

  <template>
      <child />
  </template>

  <script>
  import Child from './child'
  export default {
      name: 'ErrorTest'.data() {
          return {
              currentTime: Date.now()
          }
      },
      components: {
          Child
      },
      errorCaptured (err, vm, info) {
          console.log('vue errorCaptured: '+ err); }}</script>

  // vue errorCaptured: ReferenceError: current is not defined
  // vue errorHandler: ReferenceError: current is not defined
Copy the code

As the example above shows, errorCaptured precedes errorHandler, and you can return false in the hook function if you don’t want to be caught again. Attached is the error propagation rule given by Vue official website:

  • By default, if the global config.errorHandler is defined, all errors will still be sent to it, so these errors will still be reported to a single analysis service place.
  • If there are multiple errorCaptured hooks in a component’s descendant or parent slave links, they will be recalled one by one by the same error.
  • If the errorCaptured hook throws an error itself, both the new error and the one that was caught are sent to the global config.errorHandler.
  • An errorCaptured hook returns false to prevent an error from escalating. Essentially saying “This error has been fixed and should be ignored”. It prevents errorCaptured hooks and the global config.errorHandler that are triggered by this error.

React catches exceptions

The React website has a section on exceptions — error boundaries.

Error boundary

React error boundaries are a concept introduced in React 16 to address application crashes caused by JavaScript errors in parts of the UI.

The error boundary is a React component that catches and prints JavaScript errors that occur anywhere in its child tree, and instead of rendering the broken child tree, it renders the alternate UI. Error bounds catch errors during rendering, in lifecycle methods, and in constructors throughout the component tree. If either (or both) of the static getDerivedStateFromError() or componentDidCatch() lifecycle methods are defined in a class component, it becomes an error boundary. Only class components can be error bound components.

Based on the above description, our error bound component could be written as follows:

  import React from 'react'

  import Default from '/default'
  import { uploadError } from '.. /utils/error'

  class ErrorBoundary extends React.Component {
    constructor (props) {
      super(props)
      this.state = { hasError: false}}static getDerivedStateFromError (err) {
      // An error occurred, displaying the degraded UI
      return { hasError: true }
    }

    componentDidCatch (err, info) {
      // Error logs can be reported to the server
      uploadError(err)
    }

    render () {
      if (this.state.hasError) {
        return <Default />
      }
      return this.props.children
    }
  }

  export default ErrorBoundary

  / / use:
  <ErrorBoundary>
    <Child />
  </ErrorBoundary>
Copy the code

Error boundaries work like JavaScript’s Catch {}, except that they only apply to React components. Errors that cannot be caught by error boundaries have the following aspects. These exceptions need to be caught using try-catch, etc. :

  • The event processing
  • Asynchronous code
  • Server side rendering
  • Errors thrown by itself (not by its children)

The iframe abnormal

We can also use the onError method to catch iframe exceptions when our page references an iframe, but this form is limited to your own page and the same domain name of the iframe:

  <iframe src="./iframe.html"></iframe>
  <script>
      window.frames[0].onerror = function (message) {
          console.log('iframe error: ' + message);
          return true;
      }
  </script>

  // iframe error: Uncaught ReferenceError: a is not defined
Copy the code

Page collapse

Page crashes are not the same as the case of exception catching mentioned above. When the page crashes, the JavaScript code is no longer executing. There are, however, two ways to monitor page crashes: a combination of load and beforeUnload, and a Service worker-based approach.

Load and beforeUnload events

Let’s look at the code first:

  window.addEventListener('load'.function () {
    sessionStorage.setItem('good_exit'.'pending');
    setInterval(function () {
      sessionStorage.setItem('time_before_crash'.new Date().toString());
    }, 1000);
  });

  window.addEventListener('beforeunload'.function () {
    sessionStorage.setItem('good_exit'.'true');
  });

  if(sessionStorage.getItem('good_exit') &&
    sessionStorage.getItem('good_exit')! = ='true') {
    /* insert crash logging code here */
    alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
  }
Copy the code

From the above code, this method actually leverages the inability to trigger beforeUnload events when a page crashes. After the page is loaded, store good_exit as pending in sessionStorage. If the page closes properly, the beforeUnload event is triggered, in which we reset the value of good_exit to true. If the page crashes, when the page is refreshed, the value read from sessionStorage is pending instead of true.

Use the above method to deal with the following problems:

  1. Because it is the value stored in sessionStorage, if the user closes the page or re-opens the browser after the page crashes, we cannot obtain the good_exit value stored in sessionStorage.
  2. If you move forward or backward, the page is loaded from the cache, sometimes without triggering the load event.

Despite the above problems, this method is still instructive to us. When the page crashes, the JavaScript stops executing, the DOM is uninstalled, and there’s nothing we can do about rendering the page. However, we can capture the last crash when the user refreshes the page again and report the crash to the monitoring system. If the monitoring system receives a large number of crash messages, there is a serious problem with our page, and we need to find a way to reproduce or find the cause of the crash at the level of code logic.

Based on the Service Worker

The Service worker-based solution also uses the failure to trigger beforeUnload events when a page crashes. The difference from load and beforeUnload is that the Service Worker is relative to the main JavaScript thread driving the application. It runs in other threads and the Service Worker generally does not crash even if the page crashes. So, we don’t need to wait for the user to refresh the page again to get the last crash information.

// JavaScript code for the page
if(navigator.serviceWorker.controller ! = =null) {
  let HEARTBEAT_INTERVAL = 5 * 1000; // Heart beats every five seconds
  let sessionId = uuid();
  let heartbeat = function () {
    navigator.serviceWorker.controller.postMessage({
      type: 'heartbeat'.id: sessionId,
      data: {} // Additional information, such as the page address, is reported if the page crashes
    });
  }
  window.addEventListener("beforeunload".function() {
    navigator.serviceWorker.controller.postMessage({
      type: 'unload'.id: sessionId
    });
  });
  setInterval(heartbeat, HEARTBEAT_INTERVAL);
  heartbeat();
}

// Service Worker
const CHECK_CRASH_INTERVAL = 10 * 1000; // Check every 10 seconds
const CRASH_THRESHOLD = 15 * 1000; If there is no heartbeat after 15s, the crash is considered
const pages = {}
let timer
function checkCrash() {
  const now = Date.now()
  for (var id in pages) {
    let page = pages[id]
    if ((now - page.t) > CRASH_THRESHOLD) {
      / / report to crash
      delete pages[id]
    }
  }
  if (Object.keys(pages).length == 0) {
    clearInterval(timer)
    timer = null
  }
}

worker.addEventListener('message'.(e) = > {
  const data = e.data;
  if (data.type === 'heartbeat') {
    pages[data.id] = {
      t: Date.now()
    }
    if(! timer) { timer =setInterval(function () {
        checkCrash()
      }, CHECK_CRASH_INTERVAL)
    }
  } else if (data.type === 'unload') {
    delete pages[data.id]
  }
})

Copy the code

The code above looks like this:

  1. After the web page is loaded, it sends a heartbeat to sw every 5s through postMessage API to indicate that it is online. Sw registers the online web page and updates the registration time.
  2. Web pages in the beforeUnload, through the postMessage API to tell itself that it has been properly closed, sw will register the page cleared;
  3. If the page crashes during execution, the running state in sw will not be cleared, and the update time stays at the last heartbeat before crash.
  4. The Service Worker checks the registered web page every 10s and finds that the registration time has exceeded a certain time (such as 15s), then the web page can be judged as crashing.

Similarly, errors caught by Service workers are useful for front-end monitoring.

conclusion

Specific to the actual work, we need to deal with the following types of exceptions:

  1. Syntax errors and code exceptions: add try-catch to suspicious areas and add window.onerror to global areas;
  2. Data request exceptions: use promise-catch to handle promise exceptions, use unhandledrejection to handle uncaught Promise exceptions, use try-catch to handle async/await exceptions;
  3. Static resource loading exception: add onError to element, add window. AddEventListener globally;
  4. White screen: Vue uses errorHandler, React uses componentDidCatch, render alternate UI;
  5. Iframe exception: OnError is used in co-domain conditions.
  6. Page crashes: Load and beforeunload combine or use the Service Worker.

Original address: yolkpie.net/2021/01/28/…