likes
comments
collection
share

JavaScript错误处理终极指南

作者站长头像
站长
· 阅读数 18

本教程将深入探讨JavaScript错误处理,以便您能够抛出、检测和处理自己的错误。

不存在没有bug的系统,如果有什么问题出现,最可能的情况是第一个用户第一次访问的时候。我们可以通过以下方法避免一些Web系统的bug:

  • 一个好用的编辑器和Lint校验规则。
  • 好的验证和用户错误捕捉。
  • 好的测试流程。

然而,错误仍然存在。浏览器可能会失败或不支持我们使用的API。服务器可能会崩溃或响应时间过长。网络连接可能会失败或变得不可靠。问题可能是暂时的,但我们无法通过编码来解决此类问题。但是,我们可以预见问题,采取补救措施,并使我们的应用程序更具弹性。

显示错误消息是最后的手段

理想情况下,用户永远不应该看到错误消息。 我们可能可以忽略较小的问题,例如一些图标无法加载。我们可以通过将数据存储在本地并稍后上传来解决更严重的问题,例如Ajax数据保存失败。只有当用户有可能丢失数据时,才需要出现错误——前提是他们可以采取某些措施。 因此,需要在发生错误时捕获错误并确定最佳操作。在JavaScript应用程序中引发和捕获错误可能一开始会让人很难,但它可能比您想象的要容易。

JavaScript如何处理错误

当JavaScript语句导致错误时,它被称为引发异常。JavaScript创建并引发描述错误的Error对象。下面的函数会在dp为负数时引发错误:

// division calculation
function divide(v1, v2, dp) {

  return (v1 / v2).toFixed(dp);

}

在抛出错误之后,JavaScript解释器会检查异常处理代码。divide()函数中没有异常处理程序,因此它会检查调用该函数的函数:

// show result of division
function showResult() {

  result.value = divide(
    parseFloat(num1.value),
    parseFloat(num2.value),
    parseFloat(dp.value)
  );

}

解释器会针对调用栈上的每个函数重复此过程,直到发生以下情况之一:

  • 它找到一个异常处理程序。
  • 它达到代码的顶层。

捕获异常

我们可以使用try...catch块在divide()函数中添加异常处理程序:

// division calculation
function divide(v1, v2, dp) {
  try {
    return (v1 / v2).toFixed(dp);
  }
  catch(e) {
    console.log(`
      error name   : ${ e.name }
      error message: ${ e.message }
    `);
    return 'ERROR';
  }
}

这将执行try{}块中的代码,但是当发生异常时,会执行catch{}块并接收抛出的错误对象。注意:对于像divide()这样的基本函数,try...catch块的演示有些过分。更简单的方法是确保dp为零或更高,后面我们会看到。 如果我们需要在trycatch代码执行时运行代码,可以定义一个可选的finally{}块:

function divide(v1, v2, dp) {
  try {
    return (v1 / v2).toFixed(dp);
  }
  catch(e) {
    return 'ERROR';
  }
  finally {
    console.log('done');
  }
}

控制台输出“done”,无论计算成功还是引发错误。finally块通常执行我们需要在try块和catch块中重复执行的操作,例如取消API调用或关闭数据库连接。 try块要求有catch块、finally块或两者都有。请注意,当finally块包含return语句时,该值将成为整个函数的返回值;在trycatch块中的其他return语句将被忽略。

嵌套异常处理

如果我们在调用showResult()函数中添加一个异常处理程序会发生什么?

// show result of division
function showResult() {

  try {
    result.value = divide(
      parseFloat(num1.value),
      parseFloat(num2.value),
      parseFloat(dp.value)
    );
  }
  catch(e) {
    result.value = 'FAIL!';
  }

}

答案是……*什么都没有!*这个catch块永远不会被执行,因为divide()函数中的catch块处理了错误。 不过,我们可以在divide()函数中编程地throw一个新的Error对象,并在第二个参数的cause属性中可选地传递原始错误:

function divide(v1, v2, dp) {
  try {
    return (v1 / v2).toFixed(dp);
  }
  catch(e) {
    throw new Error('ERROR', { cause: e });
  }
}

这将触发调用函数中的catch

// show result of division
function showResult() {

  try {
    //...
  }
  catch(e) {
    console.log( e.message ); // ERROR
    console.log( e.cause.name ); // RangeError
    result.value = 'FAIL!';
  }

}

JavaScript标准错误类型

当发生异常时,JavaScript会创建并抛出一个对象,描述使用以下类型之一的错误。

SyntaxError

由语法无效的代码引发的错误,例如缺失的括号:

if condition) { // SyntaxError
  console.log('condition is true');
}

注意:像C++和Java这样的语言在编译期间报告语法错误。JavaScript是一种解释性语言,因此在代码运行之前不会识别语法错误。任何优秀的代码编辑器或Lint程序都可以在尝试运行代码之前发现语法错误。

ReferenceError

当访问不存在的变量时引发的错误:

function inc() {
  value++; // ReferenceError
}

TypeError

当值不是预期类型时引发的错误,例如调用不存在的对象方法:

const obj = {};
obj.missingMethod(); // TypeError

RangeError

当值不在允许的值集合或范围内时引发的错误。上面使用的toFixed()方法会生成此错误,因为它通常期望一个值介于0100之间:

const n = 123.456;
console.log( n.toFixed(-1) ); // RangeError

URIError

当URI处理函数(例如encodeURI()decodeURI())遇到格式不正确的URI时引发的错误:

const u = decodeURIComponent('%'); // URIError

EvalError

当将包含无效JavaScript代码的字符串传递给eval()函数时引发的错误:

eval('console.logg x;'); // EvalError

注意:请不要使用eval()!执行可能包含来自用户输入的字符串中的任意代码太危险了!

AggregateError

当多个错误包装在单个错误中时引发的错误。通常在调用操作(如Promise.all())时引发,该操作返回来自任意数量的Promise的结果。

InternalError

当JavaScript引擎内部发生错误时引发的非标准(仅限Firefox)错误。通常这是某些东西占用了太多的内存,例如一个大的数组或“过多的递归”。

Error

最后,有一个通用的Error对象,通常在实现我们自己的异常时使用……下一节我们将讨论此内容。

自定义错误

当发生错误或应该发生错误时,我们可以抛出自定义异常。例如:

  • 我们的函数未传递有效参数
  • Ajax请求未返回预期数据
  • DOM更新失败,因为该节点不存在

throw语句实际上接受任何值或对象。例如:

throw 'A simple error string';
throw 42;
throw true;
throw { message: 'An error', name: 'MyError' };

异常会被抛到调用堆栈上的每个函数,直到它们被异常(catch)处理程序拦截。然而,更实际地,我们将想要创建并抛出一个Error对象,以便它们与JavaScript抛出的标准错误相同。

我们可以通过向构造函数传递可选消息来创建通用的Error对象:

throw new Error('An error has occurred');

我们还可以像使用函数一样使用Error,而不是使用new。它返回与上面相同的Error对象:

throw Error('An error has occurred');

我们可以可选地将文件名和行号作为第二个和第三个参数传递:

throw new Error('An error has occurred', 'script.js', 99);

这很少是必要的,因为它们默认为我们抛出Error对象的文件和行号。 (它们在文件更改时也难以维护!)

我们可以定义通用的Error对象,但我们应该在可能的情况下使用标准Error类型。例如:

throw new RangeError('Decimal places must be 0 or greater');

所有Error对象都具有以下属性,我们可以在catch块中检查这些属性:

  • .name: 错误类型的名称-如ErrorRangeError
  • .message: 错误消息 以下非标准属性在Firefox中也受支持:
  • .fileName: 发生错误的文件
  • .lineNumber: 错误发生的行号
  • .columnNumber: 错误发生行的列号
  • .stack: 列出错误发生之前进行的函数调用的堆栈跟踪

我们可以将divide()函数更改为在小数位数不是数字,小于零或大于八时抛出RangeError:

// division calculation
function divide(v1, v2, dp) {

  if (isNaN(dp) || dp < 0 || dp > 8) {
    throw new RangeError('Decimal places must be between 0 and 8');
  }

  return (v1 / v2).toFixed(dp);
}

类似地,我们可以在被除数值不是数字时抛出ErrorTypeError,以防止得到NaN的结果:

  if (isNaN(v1)) {
    throw new TypeError('Dividend must be a number');
  }

我们还可以为非数字或零的除数进行处理。JavaScript在除以零时返回Infinity,但这可能会困惑用户。我们可以不使用通用的Error,而是创建一个自定义的DivByZeroError错误类型:

// new DivByZeroError Error type
class DivByZeroError extends Error {
  constructor(message) {
    super(message);
    this.name = 'DivByZeroError';
  }
}

然后以同样的方式抛出它:

if (isNaN(v2) || !v2) {
  throw new DivByZeroError('Divisor must be a non-zero number');
}

现在在调用的showResult()函数中添加一个try ... catch块。它可以接收任何Error类型并相应地做出反应-在这种情况下,显示错误消息:

// show result of division
function showResult() {

  try {
    result.value = divide(
      parseFloat(num1.value),
      parseFloat(num2.value),
      parseFloat(dp.value)
    );
    errmsg.textContent = '';
  }
  catch (e) {
    result.value = 'ERROR';
    errmsg.textContent = e.message;
    console.log( e.name );
  }

}

在JavaScript中,抛出和处理异常是一种重要的编程技巧。JavaScript的异常处理与其他编程语言的异常处理非常相似,主要使用try...catch语句块来捕获错误和异常。

同步函数错误

在同步函数中,我们可以使用throw语句抛出一个Error对象,然后使用try...catch语句块来处理错误。例如:

// division calculation
function divide(v1, v2, dp) {

  if (isNaN(v1)) {
    throw new TypeError('Dividend must be a number');
  }

  if (isNaN(v2) || !v2) {
    throw new DivByZeroError('Divisor must be a non-zero number');
  }

  if (isNaN(dp) || dp < 0 || dp > 8) {
    throw new RangeError('Decimal places must be between 0 and 8');
  }

  return (v1 / v2).toFixed(dp);
}

不再需要在最终的return语句周围放置try...catch块,因为它不应该生成错误。如果出现错误,JavaScript会生成自己的错误,并由showResult()函数中的catch块来处理。

异步函数错误

对于基于回调的异步函数,我们不能使用try...catch语句块来捕获异常,因为错误发生在try...catch块执行完成后。以下代码看起来正确,但catch块永远不会执行,控制台会在一秒钟后显示一个Uncaught Error消息:

function asyncError(delay = 1000) {

  setTimeout(() => {
    throw new Error('I am never caught!');
  }, delay);

}

try {
  asyncError();
}
catch(e) {
  console.error('This will never run');
}

大多数框架和服务器运行时(如Node.js)中的约定假定将错误作为回调函数的第一个参数返回。这不会引发异常,尽管我们可以手动抛出一个Error对象:

function asyncError(delay = 1000, callback) {

  setTimeout(() => {
    callback('This is an error message');
  }, delay);

}

asyncError(1000, e => {

  if (e) {
    throw new Error(`error: ${ e }`);
  }

});

基于Promise的错误

当使用Promise作为异步函数的返回值时,我们可以使用reject()方法返回一个新的Error对象或任何其他值来表示错误:

function wait(delay = 1000) {

  return new Promise((resolve, reject) => {

    if (isNaN(delay) || delay < 0) {
      reject( new TypeError('Invalid delay') );
    }
    else {
      setTimeout(() => {
        resolve(`waited ${ delay } ms`);
      }, delay);
    }

  })

}

当传递无效的delay参数时,Promise.catch()方法会执行,并接收到返回的Error对象:

// invalid delay value passed
wait('INVALID')
  .then( res => console.log( res ))
  .catch( e => console.error( e.message ) )
  .finally( () => console.log('complete') );

我们还可以使用await关键字调用返回Promise的任何函数。这必须在async函数内部进行,但我们可以使用标准的try...catch语句块来捕获错误:

(async () => {

  try {
    console.log( await wait('INVALID') );
  }
  catch (e) {
    console.error( e.message );
  }
  finally {
    console.log('complete');
  }

})();

异常处理

抛出Error对象并处理异常在JavaScript中非常容易:

try {
  throw new Error('I am an error!');
}
catch (e) {
  console.log(`error ${ e.message }`)
}

如果您想更深入地学习JavaScript中的错误处理,请参考以下资源: