Skip to content

Latest commit

 

History

History
executable file
·
354 lines (278 loc) · 10.3 KB

前端异常处理.md

File metadata and controls

executable file
·
354 lines (278 loc) · 10.3 KB

前端异常处理

关于异常

定义

程序发生了意想不到的情况,这种情况影响到了程序的正确运行

异常只有被抛出的时候才会产生影响

下列代码不会产生异常

function func() {
  console.log(1);
  new Error("this is an error");
  console.log(2);
}

异常类型

异常分为: 编译时异常和运行时异常

异常的传播

JS 中如果程序发生了运行时异常,会沿着调用栈一层层向上传播,直到栈空。

实现方:主动抛出异常

在实现一个基础工具库时,我们可能需要确保调用者传入的参数满足一定的条件。如果参数不符合预期,我们可以主动抛出异常来提醒调用者。

考虑一个简单的工具函数,用于计算两个数的除法:

function divideNumbers(a, b) {
  // 检查除数是否为零
  if (b === 0) {
    // 如果除数为零,主动抛出异常
    throw new Error("除数不能为零");
  }
  return a / b;
}

// 调用示例
try {
  const result = divideNumbers(10, 2);
  console.log("结果:", result);
  const resultWithZeroDenominator = divideNumbers(8, 0); // 这里会抛出异常
  console.log("不会执行到这里"); // 由于异常,这行代码不会执行
} catch (error) {
  console.error("发生错误:", error.message); // 捕获并处理异常
}

对于复杂的业务逻辑,我们可能需要抛出不同类型的异常,以便调用者能够根据异常类型来进行不同的处理。在 JavaScript 中,我们可以通过继承 Error 类来实现自定义异常类型。

class BusinessError extends Error {
  constructor(message) {
    super(message);
    this.name = "BusinessError"; // 设置异常的名称
  }
}

调用方:处理 or 继续抛出

作为调用方:在异常处理的上下文中,"处理或继续抛出"意味着在捕获到异常后,开发者可以选择在当前位置进行处理(即提供一个解决方案或记录错误日志等),也可以选择将异常继续抛出,让上层调用者或全局错误处理程序来处理这个异常。

function processUserInput(value) {
  try {
    // 模拟对用户输入进行处理的业务逻辑
    if (typeof value !== "number") {
      throw new TypeError("输入必须是数字"); // 主动抛出类型错误异常
    }

    // 业务逻辑处理...
    return value * 2;
  } catch (error) {
    // 处理异常或继续抛出
    if (error instanceof TypeError) {
      console.error("类型错误:", error.message);
      // 可以选择在这里提供一个默认值或其他补救措施
      return 0;
    } else {
      // 如果不是类型错误,将异常继续抛出
      throw error;
    }
  }
}

// 调用示例
try {
  const result = processUserInput("abc");
  console.log("结果:", result);
} catch (error) {
  console.error("发生错误:", error.message);
}

PS: 切记不要捕获了异常却不处理,这样会导致异常被吞掉,从而影响程序的正确运行。

通用异常处理

try-catch

try-catch 捕获异常的时候只能够捕获同步运行时的错误和异常,不能捕获语法错误和异步执行代码的错误和异常。

try {
  throw new Error("error");
} catch (error) {
  console.log(error);
}

运行结果如下:

如果包了一层 setTimeout,情况就不一样了

try {
  setTimeout(() => {
    throw new Error("error");
  }, 1000);
} catch (error) {
  console.log(error);
}

此时我们的 try-catch 无法捕获到异常,异常向上抛出,最终被浏览器捕获,浏览器会打印出Uncatch Error的异常信息。

运行结果如下:

window.onerror

window.onerror 捕获异常能力比 try-catch 稍微强点,无论是异步还是非异步错误,onerror 都能捕获到运行时错误

window.onerror 有一个返回值,如果返回 true,浏览器就不会再处理这个错误,如果返回 false,浏览器会继续处理这个错误,这个返回值的作用是阻止浏览器默认的错误处理行为。

window.onerror = function (message, source, lineno, colno, error) {
  console.error("全局错误捕获:", message, source, lineno, colno, error);
  // 可以在这里进行错误的上报等操作
  return true; // 阻止浏览器默认的错误处理行为
};

// 模拟一个异步代码块中的错误
setTimeout(function () {
  // 这里故意引发一个错误
  undefinedFunction(); // undefinedFunction is not defined
}, 1000);

// 模拟一个同步代码块中的错误
try {
  throw new Error("手动抛出一个错误");
} catch (error) {
  console.error("捕获同步错误:", error.message);
}

运行结果如下:

关于 window.onerror 还有两点需要注意:

1. 全局捕获的代码最好放在所有代码的最前面,比如在 html 中嵌入一个 script 标签,这样才能捕获到所有的错误

比如笔者的项目中曾这样使用

<script>
  function report(errorMsg) {
    var reportUrl = "/error";
    errorMsg.unshift(["userAgent", navigator.userAgent]);
    var params = errorMsg.reduce(function (
      accumulator,
      currentValue,
      currentIndex
    ) {
      var connectSymbol = "&";
      if (currentIndex === 0) {
        connectSymbol = "";
      }
      return (
        accumulator + connectSymbol + currentValue[0] + "=" + currentValue[1]
      );
    },
    "?");
    new Image().src = reportUrl + params;
  }
  window.onerror = function (msg, url, lineNo, columnNo, error) {
    var errorMsg = [
      ["msg", msg],
      ["src", url],
      ["error", JSON.stringify(error)],
    ];
    report(errorMsg);
  };

  window.addEventListener("unhandledrejection", function (e) {
    e.preventDefault();
    var errorMsg = [
      ["msg", "unhandled rejection"],
      ["reason", JSON.stringify(e.reason)],
    ];
    report(errorMsg);
  });
</script>

2. window.onerror 是无法捕获到网络异常的错误

Promise 错误处理

Promise 异常处理主要通过 Promise 的 .catch() 方法来进行。 此外,对于未被处理的 Promise rejection,可以使用 unhandledrejection 事件进行全局的异常处理。

const myPromise = new Promise((resolve, reject) => {
  // 模拟异步操作,这里故意抛出一个错误
  setTimeout(() => {
    reject(new Error("Promise 异常"));
  }, 1000);
});

myPromise
  .then((result) => {
    console.log("Promise 成功:", result);
  })
  .catch((error) => {
    console.error("Promise 异常捕获:", error.message);
  });

// unhandledrejection 全局异常处理
window.addEventListener("unhandledrejection", (event) => {
  console.error("未处理的 Promise rejection:", event.reason);
  // 可以在这里进行错误的上报等操作
});

// 模拟未被处理的 Promise rejection
const unhandledRejectionPromise = new Promise((resolve, reject) => {
  // 模拟异步操作,这里故意抛出一个错误
  setTimeout(() => {
    reject(new Error("未处理的 Promise rejection"));
  }, 2000);
});

运行结果如下:

Script error

因为我们在线上的版本,经常做静态资源 CDN 化,这就会导致我们常访问的⻚面跟脚本文件来自不同的域名,这时候如果没有进行额外的配置,就会容易产生 Script error。

如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。这时候,是不会有其他太多辅助信息的,但是解决思路无非如下:

跨源资源共享机制( CORS ):我们为 script 标签添加 crossOrigin 属性

<script src="http://example.com/external-script.js" crossorigin></script>

或者动态去添加 js 脚本:

const script = document.createElement("script");
script.src = "http://example.com/external-script.js";
script.crossOrigin = "anonymous";
document.body.appendChild(script);

特别注意,服务器端需要设置:Access-Control-Allow-Origin

前端框架中的异常处理

React: Error Boundaries

import React, { Component } from "react";

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  componentDidCatch(error, errorInfo) {
    this.setState({ hasError: true });
    // 在这里可以进行错误日志的上报
    console.error("捕获到React错误:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>发生错误,组件渲染失败。</h1>;
    }
    return this.props.children;
  }
}

// 在组件中使用错误边界
class MyComponent extends Component {
  render() {
    return (
      <ErrorBoundary>
        {/* 此处放置可能出现错误的组件 */}
        <ChildComponent />
      </ErrorBoundary>
    );
  }
}

Vue 中的异常处理

在 Vue 中,可以通过全局错误处理函数、组件的 errorCaptured 钩子以及一些插件来进行异常处理。

// 全局错误处理函数
Vue.config.errorHandler = function (err, vm, info) {
  // 在这里可以进行错误日志的上报
  console.error('捕获到Vue错误:', err, info);
};

// 组件中使用 errorCaptured 钩子
Vue.component('my-component', {
  errorCaptured(err, vm, info) {
    // 在这里可以进行组件内部错误的处理
    console.error('组件内部错误:', err, info);
    return false; // 阻止错误继续冒泡到全局错误处理函数
  },
  // 组件的其他配置...
});

异常上报

两种主流上报方式:

  1. 通过 Ajax 发送数据因为 Ajax 请求本身也有可能会发生异常,而且有可能会引发跨域问题,一般情况下更推荐使用动态创建 img 标签的形式进行上报。
  2. 动态创建 img 标签的形式

更多阅读