一、try…catch 语句标记一块待尝试的语句,并规定一个以上的响应应该有一个异常被抛出。如果我们抛出一个异常,try…catch语句就捕获它。
1、try…catch 语句有一个包含一条或者多条语句的try代码块,0个或1个的catch代码块,catch代码块中的语句会在try代码块中抛出异常时执行。
2、finally 代码块总会紧跟在try和catch代码块之后执行,但会在try和catch代码块之后的其他代码之前执行。

错误处理,”try..catch”

一、有一种语法结构try..catch,它使我们可以“捕获(catch)”错误,因此脚本可以执行更合理的操作,而不是死掉。

“try…catch” 语法

一、try..catch结构由两部分组成:try和catch:

  1. try {
  2. // 代码...
  3. } catch (err) {
  4. // 错误捕获
  5. }

二、它按照以下步骤执行:

  1. 首先,执行try {…}中的代码。
  2. 如果这里没有错误,则忽略catch(err):执行到try的末尾并跳过catch继续执行。
  3. 如果这里出现错误,则try执行停止,控制流转向catch(err)的开头。变量err(我们可以使用任何名称)将包含一个 error 对象,该对象包含了所发生事件的详细信息。

image.png
三、所以,try {…}块内的错误不会杀死脚本 — 我们有机会在catch中处理它。
四、让我们来看一些例子。
【示例1】没有 error 的例子:显示alert(1)和(2):

try {
  alert('Start of try runs');  // (1) <--

  // ...这里没有 error

  alert('End of try runs');   // (2) <--

} catch(err) {

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

}

【示例2】包含 error 的例子:显示(1)和(3)行的alert中的内容:

try {

  alert('Start of try runs');  // (1) <--

  lalala; // Error,变量未定义!

  alert('End of try (never reached)');  // (2)

} catch(err) {

  alert(`Error has occurred!`); // (3) <--

}

五、try..catch仅对运行时的 error 有效
1、要使得try..catch能工作,代码必须是可执行的。换句话说,它必须是有效的 JavaScript 代码。
2、如果代码包含语法错误,那么try..catch将无法正常工作,例如含有不匹配的花括号:

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

(1)JavaScript 引擎首先会读取代码,然后运行它。在读取阶段发生的错误被称为“解析时间(parse-time)”错误,并且无法恢复(从该代码内部)。这是因为引擎无法理解该代码。
3、try..catch只能处理有效代码中出现的错误。这类错误被称为“运行时的错误(runtime errors)”,有时被称为“异常(exceptions)”。
六、try..catch同步工作
1、如果在“计划的(scheduled)”代码中发生异常,例如在setTimeout中,则try..catch不会捕获到异常:

try {
  setTimeout(function() {
    noSuchVariable; // 脚本将在这里停止运行
  }, 1000);
} catch (e) {
  alert( "won't work" );
}

2、因为try..catch包裹了计划要执行的函数,该函数本身要稍后才执行,这时引擎已经离开了try..catch结构。
3、为了捕获到计划的(scheduled)函数中的异常,那么try..catch必须在这个函数内:

setTimeout(function() {
  try {
    noSuchVariable; // try..catch 处理 error 了!
  } catch {
    alert( "error is caught here!" );
  }
}, 1000);

catch块

一、捕捉块指定了一个标识符(下方语句中的catchID)来存放抛出语句指定的值;你可以用这个标识符来获取抛出的异常信息。在插入throw块时JavaScript创建这个标识符;标识符只存在于catch块的存续期间里;当catch块执行完成时,标识符不再可用。

catch (catchID) {
  statements
}

可选的 “catch” 绑定

一、如果我们不需要 error 的详细信息,catch也可以忽略它:

try {
  // ...
} catch { // <-- 没有 (err)
  // ...
}

finally块

一、可以用finally块来令你的脚本在异常发生时优雅地退出。
1、在绑定的脚本中释放资源。
【实例1】用文件处理语句打开了一个文件(服务端的JavaScript允许你进入文件)。如果在文件打开时一个异常抛出,finally块会在脚本错误之前关闭文件。

openMyFile();
try {
    writeMyFile(theData); //This may throw a error
}catch(e){
    handleError(e); // If we got a error we handle it
}finally {
    closeMyFile(); // always close the resource
}

二、用finally覆盖返回值也适用于在catch块中抛出或重新抛出的异常
【实例1】

function f() {
  try {
    throw 'bogus';
  } catch(e) {
    console.log('caught inner "bogus"');
    throw e; // this throw statement is suspended until 
             // finally block has completed
  } finally {
    return false; // overwrites the previous "throw"
  }
  // "return false" is executed now
}

try {
  f();
} catch(e) {
  // this is never reached because the throw inside
  // the catch is overwritten
  // by the return in finally
  console.log('caught outer "bogus"');
}


// OPUPUT
// => caught inner "bogus"

1、有finally块,输出
caught inner “bogus”
2、没有finally块,输出
caught inner “bogus”
caught ter “bogus”

使用 “try…catch”:真实场景中try..catch的用例

一、正如我们所知道的,JavaScript 支持JSON.parse(str)方法来解析 JSON 编码的值。
1、通常,它被用来解析从网络,从服务器或是从其他来源接收到的数据。
二、我们收到数据后,然后像下面这样调用JSON.parse:

let json = '{"name":"John", "age": 30}'; // 来自服务器的数据

let user = JSON.parse(json); // 将文本表示转换成 JS 对象

// 现在 user 是一个解析自 json 字符串的有自己属性的对象
alert( user.name ); // John
alert( user.age );  // 30

三、如果json格式错误,JSON.parse就会生成一个 error,因此脚本就会“死亡”。
四、如果这样做,当拿到的数据出了问题,那么访问者永远都不会知道原因(除非他们打开开发者控制台)。代码执行失败却没有提示信息,这真的是很糟糕的用户体验。
五、让我们用try..catch来处理这个 error:

let json = "{ bad json }";

try {

  let user = JSON.parse(json); // <-- 当出现一个 error 时...
  alert( user.name ); // 不工作

} catch (e) {
  // ...执行会跳转到这里并继续执行
  alert( "Our apologies, the data has errors, we'll try to request it one more time." );
  alert( e.name );
  alert( e.message );
}

六、在这儿,我们将catch块仅仅用于显示信息,但是我们可以做更多的事儿:发送一个新的网络请求,向访问者建议一个替代方案,将有关错误的信息发送给记录日志的设备,……。所有这些都比代码“死掉”好得多。

抛出我们自定义的 error

一、如果这个json在语法上是正确的,但是没有所必须的name属性该怎么办?
像这样:

let json = '{ "age": 30 }'; // 不完整的数据

try {

  let user = JSON.parse(json); // <-- 没有 error
  alert( user.name ); // 没有 name!

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

二、这里JSON.parse正常执行,但是缺少name属性对我们来说确实是个 error。
三、为了统一进行 error 处理,我们将使用throw操作符。

“Throw” 操作符

一、throw操作符会生成一个 error 对象。
二、语法如下:

throw <error object>

三、技术上讲,我们可以将任何东西用作 error 对象。甚至可以是一个原始类型数据,例如数字或字符串,但最好使用对象,最好使用具有name和message属性的对象(某种程度上保持与内建 error 的兼容性)。
四、JavaScript 中有很多内建的标准 error 的构造器:Error,SyntaxError,ReferenceError,TypeError等。我们也可以使用它们来创建 error 对象。
五、它们的语法是:

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

六、对于内建的 error(不是对于其他任何对象,仅仅是对于 error),name属性刚好就是构造器的名字。message则来自于参数(argument)。
例如:

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

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

七、让我们来看看JSON.parse会生成什么样的 error:

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

1、正如我们所看到的, 那是一个SyntaxError。
八、在我们的示例中,缺少name属性就是一个 error,因为用户必须有一个name。
所以,让我们抛出这个 error。

let json = '{ "age": 30 }'; // 不完整的数据

try {

  let user = JSON.parse(json); // <-- 没有 error

  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
}

1、在(*)标记的这一行,throw操作符生成了包含着我们所给定的message的SyntaxError,与 JavaScript 自己生成的方式相同。try的执行立即停止,控制流转向catch块。
2、现在,catch成为了所有 error 处理的唯一场所:对于JSON.parse和其他情况都适用。

再次抛出(Rethrowing)

一、在上面的例子中,我们使用try..catch来处理不正确的数据。但是在try {…}块中是否可能发生另一个预料之外的 error?例如编程错误(未定义变量)或其他错误,而不仅仅是这种“不正确的数据”。
例如:

let json = '{ "age": 30 }'; // 不完整的数据

try {
  user = JSON.parse(json); // <-- 忘记在 user 前放置 "let"

  // ...
} catch(err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (实际上并没有 JSON Error)
}

二、当然,一切皆有可能!程序员也会犯错。即使是被数百万人使用了几十年的开源项目中 — 也可能突然被发现了一个漏洞,并导致可怕的黑客入侵。
三、在我们的例子中,try..catch旨在捕获“数据不正确”的 error。但是实际上,catch 会捕获到所有来自于try的 error。在这儿,它捕获到了一个预料之外的 error,但是仍然抛出的是同样的”JSON Error”信息。这是不正确的,并且也会使代码变得更难以调试。
四、为了避免此类问题,我们可以采用“重新抛出”技术。规则很简单:
五、catch应该只处理它知道的 error,并“抛出”所有其他 error。
六、“再次抛出(rethrowing)”技术可以被更详细地解释为:

  1. Catch 捕获所有 error。
  2. 在catch(err) {…}块中,我们对 error 对象err进行分析。
  3. 如果我们不知道如何处理它,那我们就throw err。

七、通常,我们可以使用instanceof操作符判断错误类型:

try {
  user = { /*...*/ };
} catch(err) {
  if (err instanceof ReferenceError) {
    alert('ReferenceError'); // 访问一个未定义(undefined)的变量产生了 "ReferenceError"
  }
}

1、我们还可以从err.name属性中获取错误的类名。所有原生的错误都有这个属性。另一种方式是读取err.constructor.name。
八、在下面的代码中,我们使用“再次抛出”,以达到在catch中只处理SyntaxError的目的:

let json = '{ "age": 30 }'; // 不完整的数据
try {
  let user = JSON.parse(json);

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

  blabla(); // 预料之外的 error

  alert( user.name );

} catch(e) {
  if (e instanceof SyntaxError) {
    alert( "JSON Error: " + e.message );
  } else {
    throw e; // 再次抛出 (*)
  }

}

1、如果(*)标记的这行catch块中的 error 从try..catch中“掉了出来”,那么它也可以被外部的try..catch结构(如果存在)捕获到,如果外部不存在这种结构,那么脚本就会被杀死。
2、所以,catch块实际上只处理它知道该如何处理的 error,并“跳过”所有其他的 error。
十、下面这个示例演示了这种类型的 error 是如何被另外一级try..catch捕获的:

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

  try {
    // ...
    blabla(); // error!
  } catch (e) {
    // ...
    if (!(e instanceof SyntaxError)) {
      throw e; // 再次抛出(不知道如何处理它)
    }
  }
}

try {
  readData();
} catch (e) {
  alert( "External catch got: " + e ); // 捕获了它!
}

1、上面这个例子中的readData只知道如何处理SyntaxError,而外部的try..catch知道如何处理任意的 error。

try…catch…finally

一、try..catch结构可能还有一个代码子句(clause):finally。
1、如果它存在,它在所有情况下都会被执行:

  • try之后,如果没有 error,
  • catch之后,如果没有 error。

二、该扩展语法如下所示:

try {
   ... 尝试执行的代码 ...
} catch(e) {
   ... 处理 error ...
} finally {
   ... 总是会执行的代码 ...
}

【示例1】

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

1、这段代码有两种执行方式:
(1)如果你对于 “Make an error?” 的回答是 “Yes”,那么执行try -> catch -> finally。
(2)如果你的回答是 “No”,那么执行try -> finally。
三、finally子句(clause)通常用在:当我们开始做某事的时候,希望无论出现什么情况都要完成完成某个任务。
【示例1】我们想要测量一个斐波那契数字函数fib(n)执行所需要花费的时间。通常,我们可以在运行它之前开始测量,并在运行完成时结束测量。但是,如果在该函数调用期间出现 error 该怎么办?特别是,下面这段fib(n)的实现代码在遇到负数或非整数数字时会返回一个 error。
1、无论如何,finally子句都是一个结束测量的好地方。
2、在这儿,finally能够保证在两种情况下都能正确地测量时间 — 成功执行fib以及fib中出现 error 时:

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 occurred");

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

3、你可以通过运行上面这段代码并在prompt弹窗中输入35来进行检查 — 代码运行正常,先执行try然后是finally。如果你输入的是-1— 将立即出现 error,执行将只花费0ms。以上两种情况下的时间测量都正确地完成了。
4、换句话说,函数fib以return还是throw完成都无关紧要。在这两种情况下都会执行finally子句。
四、变量和try..catch..finally中的局部变量
1、上面代码中的result和diff变量都是在try..catch之前声明的。
2、否则,如果我们使用let在try块中声明变量,那么该变量将只在try块中可见。
五、finally和return
1、finally子句适用于try..catch的任何出口。这包括显式的return。
2、在下面这个例子中,在try中有一个return。在这种情况下,finally会在控制转向外部代码前被执行。

function func() {

  try {
    return 1;

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

alert( func() ); // 先执行 finally 中的 alert,然后执行这个 alert

六、没有catch子句的try..finally结构也很有用。当我们不想在这儿处理 error(让它们 fall through),但是需要确保我们启动的处理需要被完成。

function func() {
  // 开始执行需要被完成的操作(比如测量)
  try {
    // ...
  } finally {
    // 完成前面我们需要完成的那件事儿,即使 try 中的执行失败了
  }
}

1、上面的代码中,由于没有catch,所以try中的 error 总是会使代码执行跳转至函数func()外。但是,在跳出之前需要执行finally中的代码。

全局 catch

一、设想一下,在try..catch结构外有一个致命的 error,然后脚本死亡了。这个 error 就像编程错误或其他可怕的事儿那样。
1、有什么办法可以用来应对这种情况吗?我们可能想要记录这个 error,并向用户显示某些内容(通常用户看不到错误信息)等。
2、规范中没有相关内容,但是代码的执行环境一般会提供这种机制,因为它确实很有用。
(1)Node.JS 有process.on(“uncaughtException”)
(2)在浏览器中,我们可以将将一个函数赋值给特殊的window.onerror属性,该函数将在发生未捕获的 error 时执行。
二、语法如下:

window.onerror = function(message, url, line, col, error) {
  // ...
};
  • message:Error 信息。
  • url:发生 error 的脚本的 URL。
  • line,col:发生 error 处的代码的行号和列号。
  • error:Error 对象。

【示例1】

<script>
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n At ${line}:${col} of ${url}`);
  };

  function readData() {
    badFunc(); // 啊,出问题了!
  }

  readData();
</script>

三、全局错误处理程序window.onerror的作用通常不是恢复脚本的执行 — 如果发生编程错误,那这几乎是不可能的,它的作用是将错误信息发送给开发者。
四、也有针对这种情况提供错误日志的 Web 服务,例如https://errorception.comhttp://www.muscula.com
1、它们会像这样运行:
(1)我们注册该服务,并拿到一段 JS 代码(或脚本的 URL),然后插入到页面中。
(2)该 JS 脚本设置了自定义的window.onerror函数。
(3)当发生 error 时,它会发送一个此 error 相关的网络请求到服务提供方。
(4)我们可以登录到服务方的 Web 界面来查看这些 error。