Javascript错误处理——try...catch

Javascript错误处理——try…catch

无论我们编程多么精通,脚本错误怎是难免。可能是我们的错误造成,或异常输入,错误的服务器端响应以及无数个其他原因。

通常,当发送错误时脚本会立刻停止,打印至控制台。

try...catch语法结构可以捕获错误并可以做更多的事情,而不是直接停止。

“try…catch” 语法

try...catch结构有两个语句块,即try,然后catch

try {

  // code...

} catch (err) {

  // error handling

}

工作流程如下:

  1. 首先try{...}代码块执行。
  2. 如果没有错误,那么catch(err)被忽略:执行到try结尾时,跳过catch块。
  3. 如果发生错误,那么try块中执行停止,控制流进入catch(err).err(可以是任何名称)变量包含错误发生相关的信息信息对象。

Javascript错误处理——try...catch_第1张图片

所以,try{...}块内的错误不会让脚本停止:我们有机会在catch块内处理。让我们看更多的示例。

  • 无错误示例:显示alert (1)(2):

    try {

    alert(‘Start of try runs’); // (1) <–

    // …no errors here

    alert(‘End of try runs’); // (2) <–

    } catch(err) {

    alert(‘Catch is ignored, because there are no errors’); // (3)

    }

    alert(“…Then the execution continues”);

  • 带错误示例:显示 (1)(3):

    try {

    alert(‘Start of try runs’); // (1) <–

    lalala; // error, variable is not defined!

    alert(‘End of try (never reached)’); // (2)

    } catch(err) {

    alert(Error has occured!); // (3) <–

    }

    alert(“…Then the execution continues”);

try..catch仅作用在运行时错误

try..catch,代码必须是运行时,换句话说,必须是有效的Javascript代码。
如果代码语法错误不会工作,举例,下面不会捕获大括号错误:

try {
  {{{{{{{{{{{{
} catch(e) {
  alert("The engine can't understand this code, it's invalid");
}

Javascript引擎首先读代码,然后运行。发生在读阶段错误称为“解析时”错误,不可恢复,因为引擎不理解代码。

所以,try...catch仅能处理有效代码中的错误,被称为“运行时”错误,有时也称为“异常”。

try..catch同步执行

如果在预定的(scheduled)代码发生异常,如setTimeout,那么try...catch不捕获异常:

try {
  setTimeout(function() {
    noSuchVariable; // script will die here
  }, 1000);
} catch (e) {
  alert( "won't work" );
}

因为try...catch包装了setTimeout调用预定函数。但函数会延后执行,此时引擎已经立刻了try...catch结构。

为了捕获预定执行函数内的异常,try...catch必须在函数内部:

setTimeout(function() {
  try {
    noSuchVariable; // try..catch handles the error!
  } catch (e) {
    alert( "error is caught here!" );
  }
}, 1000);

错误对象

当错误发生时,Javascript生成包含细节信息的对象,并作为参数传递给catch块:

try {
  // ...
} catch(err) { // <-- the "error object", could use another word instead of err
  // ...
}

对所有内置错误,catch内的错误对象主要有两个属性:

name
错误名称,一个未定义变量是“ReferenceError”

message
错误信息的文本描述。

在大多数环境中有其他非标准属性,被广泛使用和支持的一个是:stack

当前调用栈:关于导致错误的嵌套调用序列,用于调试目的。
举例:

try {
  lalala; // error, variable is not defined!
} catch(err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at ...

  // Can also show an error as a whole
  // The error is converted to string as "name: message"
  alert(err); // ReferenceError: lalala is not defined
}

使用“try…catch”

让我们探索一个真实的用例:

我们知道,Javascript支持方法JSON.parse(str),用于读json值。通常用于解析从网络中接收json数据,如服务器端或其他来源。接收并调用JSON.parse,如下:

let json = '{"name":"John", "age": 30}'; // data from the server

let user = JSON.parse(json); // convert the text representation to JS object

// now user is an object with properties from the string
alert( user.name ); // John
alert( user.age );  // 30

如果json非标准的,JSON.parse产生错误,脚本为停止。这样我们会满意吗?当然不会!

如果数据带有某种错误,用户完全不知道发送什么(除非打开开发控制台)。没人喜欢发生错误时脚本停止且没有任何错误信息。

让我们使用try...catch处理错误:

let json = "{ bad json }";

try {

  let user = JSON.parse(json); // <-- when an error occurs...
  alert( user.name ); // doesn't work

} catch (e) {
  // ...the execution jumps here
  alert( "Our apologies, the data has errors, we'll try to request it one more time." );
  alert( e.name );
  alert( e.message );
}

这里我们使用catch块仅显示信息,也可以做更多:新的网络请求,建议另一种选择,发送错误信息至日志等,总之都比代码直接停止好。

抛出我们自己的错误

如果json语法正在,但没有需要的name属性,会怎么样?

如下:

let json = '{ "age": 30 }'; // incomplete data

try {

  let user = JSON.parse(json); // <-- no errors
  alert( user.name ); // no name!

} catch (e) {
  alert( "doesn't execute" );
}

这里 JSON.parse执行正常,但缺少name属性,实际对我们来说是个错误。为了统一错误处理,我们需要使用throw操作。

Throw操作

该操作产生一个错误。语法:

throw 

技术上,可以使用任何内容作为错误对象。可以是原始类型,如数字或字符串,但最好使用对象,并带有name和message属性(与内置错误对象兼容)。

Javascript有很多内置标准错误构造器:Error、SyntaxError、ReferenceError、TypeError等其他。我们也能使用他创建错误对象。

语法:

let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

对内置错误对象(仅为错误对象),name属性正好是构造函数的名称,message是构造函数参数。

let error = new Error("Things happen o_O");

alert(error.name); // Error
alert(error.message); // Things happen o_O

让我们看JSON.parse生成的这种错误:

try {
  JSON.parse("{ bad json o_O }");
} catch(e) {
  alert(e.name); // SyntaxError
  alert(e.message); // Unexpected token o in JSON at position 0
}

如我们所见,错误为:SyntaxError

在我们的示例中,缺省name属性,也可以视为语法错误,假设users必须有个name属性,所以我们抛出错误:

let json = '{ "age": 30 }'; // incomplete data

try {

  let user = JSON.parse(json); // <-- no errors

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name"); // (*)
  }

  alert( user.name );

} catch(e) {
  alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
}

在星号行,throw操作产生SyntaxError错误,并带有给定的message,与Javascript自身生成的错误一致。try块中的执行立刻停止,控制流跳至catch块。

现在catch变成了独立处理所有错误的块:JSON.parse和其他错误。

再次抛出错误

上面的示例,我们使用try...catch处理不正确的数据,但也可能是其他异常发生在try...catch块中,如变量未定义或其他,不仅是“不正确的数据”。

如下:

let json = '{ "age": 30 }'; // incomplete data

try {
  user = JSON.parse(json); // <-- forgot to put "let" before user

  // ...
} catch(err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (not JSON Error actually)
}

当然,一切都是可能的!程序人员造成的错误,即使被大量使用的开源工具——可能会突然发现一个疯狂的bug,导致了可怕的黑客攻击(就像使用ssh工具发生的那样)。

在我们的示例中,try...catch是为了处理不正确数据错误,但实际上,catch捕获try块中所有的错误。如有一个异常错误,仍然显示“JSON Error”消息,这样的错误也使代码更难调试。

幸运的是,我们能发现捕获的错误是那种类型,示例,从其name属性:

try {
  user = { /*...*/ };
} catch(e) {
  alert(e.name); // "ReferenceError" for accessing an undefined variable
}

规则很简单。

应该仅处理已知错误,重新抛出其他错误
详细的重新抛出错误解释如下:

  1. 捕获所有错误
  2. catch(err){...}块中,我们分析错误对象err
  3. 如果不知道如何处理,那么通过throw err抛出错误

下面的代码,我们使用重新抛出错误,这样catch仅处理SyntaxError:

let json = '{ "age": 30 }'; // incomplete data
try {

  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name");
  }

  blabla(); // unexpected error

  alert( user.name );

} catch(e) {

  if (e.name == "SyntaxError") {
    alert( "JSON Error: " + e.message );
  } else {
    throw e; // rethrow (*)
  }

}

catch块内部星号行抛出错误,其可以被外部的try...catch结构块捕获(如果存在),或直接停止脚本。

所以catch块实际上仅处理已知错误,并忽略所有其他错误。

下面示例演示这样的错误被多级try...catch块处理。

function readData() {
  let json = '{ "age": 30 }';

  try {
    // ...
    blabla(); // error!
  } catch (e) {
    // ...
    if (e.name != 'SyntaxError') {
      throw e; // rethrow (don't know how to deal with it)
    }
  }
}

try {
  readData();
} catch (e) {
  alert( "External catch got: " + e ); // caught it!
}

这里readData仅知道如何处理SyntaxError错误,而外部的try...catch知道如何处理任何错误。

try…catch…finally

等等,还没有完。

结构try...catch可以有多个代码子句:finally,如果存在,所有情况都会执行。

  • try之后,如果没有错误情况
  • catch之后,如果有错误

扩展语法类似如下:

try {
   ... try to execute the code ...
} catch(e) {
   ... handle errors ...
} finally {
   ... execute always ...
}

请尝试运行下面代码:

try {
  alert( 'try' );
  if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
  alert( 'catch' );
} finally {
  alert( 'finally' );
}

代码有两条执行路径:

  1. 如果回答“Yes”产生一个错误,那么执行路径为try->catch->finally.
  2. 如果回调“No”,那么路径为try->finally.

子句finally通常应用场景为:在try...catch块之前开始做某事,无论结果如何都需要终止。

举例,我们想衡量斐波拉切函数fib(n)执行时间,很自然,我们需要在执行前和结束后衡量。但如果在函数调用期间有错误?特别是,下面代码中的fib(n)的实现将返回一个针对负数或非整数的错误。

不管发生什么,子句finally很适合去完成时间测量。

finally负责在两种场景下测试执行时间——成功执行fib函数和错误情况:

let num = +prompt("Enter a positive integer number?", 35)

let diff, result;

function fib(n) {
  if (n < 0 || Math.trunc(n) != n) {
    throw new Error("Must not be negative, and also an integer.");
  }
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}

let start = Date.now();

try {
  result = fib(num);
} catch (e) {
  result = 0;
} finally {
  diff = Date.now() - start;
}

alert(result || "error occured");

alert( `execution took ${diff}ms` );

你可以根据提示输入35,检查代码运行——执行正常,try之后执行finally。再次输入-1,立刻产生错误,执行花费0ms,正确完成时间测量。

换句话说,有两种方法可以退出函数:要么return,要么抛出错误。finally子句柄都会处理。

在try…catch…finally块中的变量是局部变量

注意在上面代码resultdiff变量,是在try...catch块之前声明的。

否则,如果使用let在{…}块里面,则只能在块内部可见。

finally 和 return

try...catch块无论如何结束,finally子句都执行。包括显示的return方式返回。

下面的示例,在try中有return,在这种情况,在控制返回外部代码之前,finally被执行。

function func() {

  try {
    return 1;

  } catch (e) {
    /* ... */
  } finally {
    alert( 'finally' );
  }
}

alert( func() ); // first works alert from finally, and then this one

try…finally
try…finally结构,没有catch子句,也有用。当我们不想在这里处理错误时可以应用,但是要确保开始和最终过程被执行。

function func() {
  // start doing something that needs completion (like measurements)
  try {
    // ...
  } finally {
    // complete that thing even if all dies
  }
}

在上面的代码中,try块的错误总会发生,因为没有catch块,但finally在执行流跳出外部之前会执行。

全局错误捕获

环境规范
本节中的信息不是核心JavaScript的一部分。

我们想像在try...catch块之外有个致命错误,那么代码会立刻停止。如编程错误或其他更糟糕的事情。

是否有应对此类事件的方法?我们可能想记录错误,向用户显示一些信息(通常他不会看到错误消息)等等。

Javascript规范中没有涉及,但环境通常都提供实现,因为确实有用。如,Node.JS有process.on(“uncaughtException”),在浏览器中可以给window.onerror赋值一个函数。它将在未捕获错误的情况下运行。

语法:

window.onerror = function(message, url, line, col, error) {
  // ...
};

message
错误信息

url
发生错误脚本url

line, col

错误发生在代码中行、列数

error
错误对象


全局错误处理window.error的角色通常不能恢复脚本执行,在编程错误的情况下是不可能的,但会给开发者发送错误信息。

也有一些web服务为这种情况提供了错误日志记录,如 https://errorception.com 或 http://www.muscula.com.

工作流程如下:

  1. 注册服务,然后获得一段JS脚本,插入至页面中。
  2. 该JS脚本中有自定义的window.error函数。
  3. 当错误发生时,会给服务器端发送网络请求。
  4. 我们可以登录服务的web界面查看错误信息。

总结

结构try...catch可以处理运行时错误,字面理解为尝试运行代码,然后捕获可能发生的错误。

语法为:

try {
  // run this code
} catch(err) {
  // if an error happened, then jump here
  // err is the error object
} finally {
  // do in any case after try/catch
}

也可能没有catchfinally块,所以try...catchtry...finally都是有效语法。

错误对象有下面属性:

  • message——用户可以理解的错误信息。
  • name——错误名称字符串(错误构造函数名称)。
  • stack——非标准——发生错误是的堆栈信息。

我们也能通过使用throw操作生成自己的错误,技术上,throw的参数可以是任意类型,但通常是从内置错误类Error继承的对象。后面会介绍扩展错误对象。

再次抛出是错误处理的基本模式:catch块通常期望并知道怎样处理特定的错误,所以应该重新抛出未知错误。

即使我们没有使用try...catch,大多数环境也支持设置全局的错误处理,捕获所有发生的错误,浏览器内置的是window.onerror.

你可能感兴趣的:(深入理解Javascript)