1. 程式人生 > >Javascript錯誤處理——try...catch

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(可以是任何名稱)變數包含錯誤發生相關的資訊資訊物件。

所以,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 <error object>

技術上,可以使用任何內容作為錯誤物件。可以是原始型別,如數字或字串,但最好使用物件,並帶有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
錯誤物件

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

  function readData() {
    badFunc(); // Whoops, something went wrong!
  }

  readData();
</script>

全域性錯誤處理window.error的角色通常不能恢復指令碼執行,在程式設計錯誤的情況下是不可能的,但會給開發者傳送錯誤資訊。

工作流程如下:

  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.