Error handling
Error handling, "try...catch"
ไม่สำคัญว่า เราจะเขียนโปรแกรมเก่งแค่ไหน บางที script ของเรา บางทีมันอาจเกิดชึ้นจากความผิดพลาดของเรา, input ที่เราไม่ได้คาดไม่ถึงจาก user, error จาก user และอีกหลาย ๆ อย่าง
และทำให้ script ตาย และทำให้ error แสดงผลออกมาใน console
แต่ syntax try...catch ทำให้เราสามารถจับ Error ได้ และแทนที่ script เราจะตาย เราจะทำอะไรบางอย่างกับมันแทน
The “try…catch” syntax
try...catch มี 2 block คือ try และ catch
try {
// code...
} catch (err) {
// error handling
}
มันทำงานอย่างนี้
- โค้ดใน
try {...}ถูกรัน - ถ้ามันไม่มี Error ดังนั้น
catch (err)ก็จะไม่ได้ทำอะไร, มันจะรันไปจนจบtryและข้ามcatchไป - ถ้ามี Error ดังนั้น
tryจะหยุดการทำงาน และจะเปลี่ยนไปทำงานใน block ของcatchแทน,errเอาไว้บอกรายละเอียดของ Error

ดังนั้น ถ้าเกิด Error ขึ้นใน try {...} script มันจะไม่ตาย แต่เราจะสามารถรับมือกับ Error ได้
เรามาดูตัวอย่างการใช้งานกัน
-
ตัวอย่างที่ไม่มี Error: โค้ดจะแสดง
(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)
} -
ตัวอย่างที่มี Error จะแสดง
(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 occurred!`); // (3) <--
}
Error Object
เมื่อเกิด Error ขึ้น JavaScript จะสร้าง object เพื่อเก็บรายละเอียดของ Error ไว้ และส่งมาเป็น argument ใน catch
try {
// ...
} catch (err) { // <-- the "error object", could use another word instead of err
// ...
}
error object มี properties หลัก 2 ตัว:
name
เป็นชื่อของ Error ตัวอย่างเช่น ตัวแปรที่เป็น undefined คือ "ReferenceError".
message
ข้อความที่บอกรายละเอียดของ Error
และก็มี non-standard properties ที่ใช้กันอย่างแพร่หลาย
stack
การเรียกใช้ stack: string ที่มากับข้อมูลเกี่ยวกับ ในสำหรับการ debugging
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 (...call stack)
// Can also show an error as a whole
// The error is converted to string as "name: message"
alert(err); // ReferenceError: lalala is not defined
}
Optional “catch” binding
ถ้าเราไม่ต้องการรายละเอียดของ Error เราก็สามารถทำได้
try {
// ...
} catch { // <-- without (err)
// ...
}
Using “try…catch”
เรามาดู use case จริงของ try...catch กัน
อย่างที่เรารู้ JSON.parse(str) method ใช้อ่าน JSON ที่ยังเป็น JSON format อยู่
มันใช้เพื่อ decode JSON และรับข้อมูลบน network จาก server
เราสามารถรับและเรียกใช้ 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 methods, toJSON chapter.
ถ้า JSON มาผิดรูปแบบ มันจะทำให้เกิด Error และ script จะตาย
และเราควรพอใจหรือไม่ ไม่!
วิธีนี้ ถ้ามีบางอย่างผิดพลาดกับข้อมูลที่ส่งมา คนที่เขียนโค้ดจะไม่สามารถรู้ได้เลย (เว้นแต่ว่าง เราจะเปิดดู console) และคนแบบเรา ๆ คงไม่ชอบให้โค้ดเราตายไปเฉย ๆ โดยไม่บอกอะไรเลย
ดังนั้น เรามาใช้ try...catch ในการ handle มันกัน
let json = "{ bad json }";
try {
let user = JSON.parse(json); // <-- when an error occurs...
alert( user.name ); // doesn't work
} catch (err) {
// ...the execution jumps here
alert( "Our apologies, the data has errors, we'll try to request it one more time." );
alert( err.name );
alert( err.message );
}
เมื่อเราใช้ catch block จะโชว์ข้อความออกมา แต่เราสามารถทำได้มากว่านั้น เช่น การส่ง request ไปใหม่, แนะนำทางเลือกให้กับ user, ส่งข้อมูลเกี่ยวกับ Error กลับไป ดีกว่าปล่อยมันตายไปเฉย ๆ
Throwing our own errors
จะเกิดอะไรขึ้นเมื่อ json มาถูกรูปแบบแล้ว แต่ว่าไม่มี properties ที่เราต้องการ
เช่น:
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
alert( user.name ); // no name!
} catch (err) {
alert( "doesn't execute" );
}
JSON.parse จะรันได้ปกติ แต่การเข้าถึง name ทำให้เกิด Error ขึ้น
ดังนั้นเราจะโยน Error โดยใช้ throw operator
“Throw” operator
throw operator จะสร้าง Error ขึ้นมา
syntax:
throw <error object>
ตามเทคนิคแล้ว เราสามารถใช้อะไรก็ได้ในการเป็น Error แม้ว่าจะเป็น Primitive type เช่น string number แต่เราควรใช้เป็น Object จะดีกว่า เพราะว่ามันจะมี name และ message properties
JavaScript มี built-in constructors สำหรับ Error เช่น: Error, SyntaxError, ReferenceError, TypeError และอื่น ๆ เราสามารถใช้สร้างเป็น Object ได้เช่นกัน
let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...
สำหรับ built-in error name property จะเป็นตาชื่อของ constructor และ message จะเอามาจาก argument ที่เราป้อนเข้าไป
ตัวอย่างเช่น:
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 (err) {
alert(err.name); // SyntaxError
alert(err.message); // Unexpected token b in JSON at position 2
}
อย่างที่เราเห็นมันเป็น SyntaxError
เรามา throw มันกัน:
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 (err) {
alert( "JSON Error: " + err.message ); // JSON Error: Incomplete data: no name
}
ในบรรทัด (*) throw operator สร้าง SyntaxError กับ message ที่เราให้ไป เหมือนกับที่ JavaScript สร้าง Error ขึ้นมา และ โค้ดใน try จะหยุดโดยทันทีและ มันจะกระโดดไปที่ catch
Rethrowing
ในตัวอย่างด้านบน เราใช้ try...catch ในการจับ Error แต่มันจะเป็นไปได้ไหม ที่จะเกิด Error ที่เราไม่ได้คาดไว้ใน try {...} block เหมือนกับการเขียนโปรแกรมผิดพลาด (variable is not defined) หรืออย่างอื่น ๆ ที่ไม่ได้มีแค่ “incorrect data”
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
// (no JSON Error actually)
}
แน่นอนว่ามันเกิดขึ้นได้ โปรแกรมเมอร์สามารถทำผิดพลาดได้ ไม่เว้นแต่ open-source utilities ที่เปิดให้คนเป็นล้านใช้ ก็ยังเจอบัค ที่ทำให้นำไปสู่การแฮกได้
ในกรณีของเรา try...catch วางไว้เพื่อจับ incorrect data Error แต่โดยธรรมชาติแล้ว catch รับ errors จาก try และมันก็ทำให้เกิด unexpected error แต่ก็ยังโชว์ "JSON Error" เหมือนเดิม นั่นผิด และทำให้การ bedug ยากขึ้น
ในการแจ้งเตือนปัญหา เราสามารถใช้ “rethrowing” เทคนิคได้
การ “rethrowing” เทคนิคสามารถใช้เพื่ออธิบายรายละเอียดของ Error ได้
- จับ Error ทั้งหมด
- ใน
catch (err) {...}เราจะวิเคราะห์ประเภทของ Error - ถ้าเราไม่รู้ Error เราสามารถ rethrow ได้
try {
user = { /*...*/ };
} catch (err) {
if (err instanceof ReferenceError) {
alert('ReferenceError'); // "ReferenceError" for accessing an undefined variable
}
}
เราสามารถได้รับ error class name จาก err.name property ได้ ทางเลือกอื่น คือใช้ อ่าน err.constructor.name.
ในโค้ดด้านล่าง เราใช้การ rethrowing ดังนั้น catch จะ handles แค่ 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 (err) {
if (err instanceof SyntaxError) {
alert( "JSON Error: " + err.message );
} else {
throw err; // rethrow (*)
}
}
error throwing ในบรรทัด (*) จากด้านใน block หล่นออกจาก try...catch และสามารถถูกจับได้โดย try...catch ภายใน (ถ้ามี) หรือไม่มันก็ฆ่า script
ดังนั้น catch block จะรับมือแค่ errors ที่รู้ว่าจะรับมืออย่างไร และข้ามอันที่ไม่รู้ทั้งหมด
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // error!
} catch (err) {
// ...
if (!(err instanceof SyntaxError)) {
throw err; // rethrow (don't know how to deal with it)
}
}
}
try {
readData();
} catch (err) {
alert( "External catch got: " + err ); // caught it!
}
readData จะรู้วิธีการรับมือกับ SyntaxError ในขณะที่ try...catch จะรับมือ Error ทุกรูปแบบ
try…catch…finally
เดี๋ยวก่อน แต่นั่นไม่ใช่ทั้งหมด
try...catch ยังมีอีก 1 block คือ finally
ถ้ามันมี มันจะรัันในทุกกรณี
- หลังจาก
tryถ้าไม่มี Error - หลังจาก
catchถ้ามี Error
syntax จะเป็นประมาณนี้
try {
... try to execute the code ...
} catch (err) {
... handle errors ...
} finally {
... execute always ...
}
ในการรันโค้ด
try {
alert( 'try' );
if (confirm('Make an error?')) BAD_CODE();
} catch (err) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
โค้ดนี้มี execution สองทาง
- ถ้าคำตอบคือใช่ ดังนั้น
try -> catch -> finally. - ถ้าไม่
try -> finally.
finally จะถูกใช้เมื่อ เราเริ่มทำบางอย่าง และต้องการจะจัดการในทุกกรณีของโค้ด
ตัวอย่างเช่น วัดเวลาของ function Fibonacci fib(n) ว่าใช้เวลาเท่าไหร่ เราเริ่มวัดได้ก่อนมันจะรัน และเสร็จการทำงาน แต่จะเกิดอะไรขึ้นเมื่อมี Error ของเราในโค้ด ในทางปฏิบัติแล้ว ในการเขียนโค้ดด้านล่าง fib(n) ของโค้ดด้านล่าง จะ return Error ออกมา
finally เป็นสิ่งที่ใช้วัดเวลา โดยไม่สนสิ่งอื่นใด
finally รับประกันว่า เวลานั้นจะสามารถวัดได้อย่างถูกต้อง ไม่ว่าจะในกรณีไหนก็ตาม
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 (err) {
result = 0;
} finally {
diff = Date.now() - start;
}
alert(result || "error occurred");
alert( `execution took ${diff}ms` );
คุณสามารถตรวจสอบได้ว่า เมื่อเราส่งค่า 35 เข้าไป มันจะรันปกติ และ finally จะรันหลัง try และถ้าเราใส่ -1 เข้าไป ก็จะเกิด Error ขึ้นโดยทันที และเวลา execution จะใช้แค่ 0ms โดยทั้งสองจะวัดได้อย่างถูกต้อง
ในอีกความหมายหนึ่ง function อาจจะเสร็จด้วย return หรือ throw นั่นไม่สำคัญ เพราะ finally จะรันอยู่ดี ในทั้ง 2 กรณี
Global catch
จิตนาการว่า เรามี Error นอก try...catch และ script เราตายเช่น programming error หรือ บางอย่างที่แย่
มันมีทางที่เราจะสามารถจับ Error ได้หรือไม่ เราอาจต้องการที่จะบันทึก Error ไว้ หรือแสดงบางอย่างไปที่ user
มันไม่มีข้อกำหนดไว้ แต่ว่า environments มักจะกำหนดสิ่งนี้มาให้ เพราะว่ามันสำคัญจริง ๆ ตัวอย่างเช่น Node.js จะมี process.on("uncaughtException") ให้ และใน Browser สามารถใส่ function window.onerror property และมันจะทำงานเมื่อมี Error ที่จับไม่ได้เกิดขึ้น
window.onerror = function(message, url, line, col, error) {
// ...
};
message
ข้อความ Error
url
URL ของ script ว่าเกิดขึ้นที่ไหน
line, col
Line และ column ว่ามันเกิดขึ้นที่ตรงไหน
error
Error object.
ตัวอย่างเช่น:
<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.onerror ไม่ใช่การแก้ไข execution เพราะว่านั่นมันเป็นไปไม่ได้เลย แต่ว่ามันจะส่งข้อความ error ไปที่ developers แทน
Summary
try...catch อนุญาตให้เรารับมือกับ Error ได้ มันให้เราได้ “ลอง” การทำงานของโค้ด และ “จับ” Error ถ้ามันเกิดขึ้น
syntax:
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
}
มันอาจไม่มี catch หรือไม่มี finally, หรือการเขียน try...catch และ try...finally ก็สามารถเขียนได้
Error objects มี properties ตามนี้
message– ข้อความที่ให้เราอ่านได้.name– ชื่อของ Error (ถ้าเป็น built-in Error ชื่อจะเป็นชื่อตาม Contructor).stack(non-standard, แต่ใช้งานได้ดี) — ใช้กับการอ่าน Error
ถ้าเราไม่ต้องการใช้ error object เราสามารถเปลี่ยนเป็น catch { แทนที่จะเป็น catch (err) {.
เราสามารถสร้าง Error ขึ้นได้ โดยใช้ throw โดย throw สามารถเป็นได้ทุกประเภท แต่โดยปกติแล้วเรามักจะสร้าง error object โดยใช้ built-in Error class
Rethrowing เป็นสิ่งที่สำคัญมากของ error handling catch ควรใช้เมื่อเรารู้ว่า Error เป็นประเภทไหน และถ้าเราไม่รู้ เราควรจะ Rethrowing
ถ้าเราไม่มี try...catch environments ส่วนใหญ่ จะมี “global” error handler ในเราได้ใช้จับ Error ที่ไม่ได้อยู่ใน try...catch ใน Browser เราจะใช้ window.onerror
Custom errors, extending Error
เมื่อเราพัฒนาอะไรซักอย่าง หลาย ๆ ครั้ง เราต้องการ Error class ของเราในการเจาะจง Error ที่ผิดพลาดในโค้ดของเรา สำหรับ Network เราต้องการ HttpError , สำหรับฐานข้อมูล DbError , สำหรับการ searching เราอาจใช้ NotFoundError และ อื่น ๆ
และ Error ของเราควรรองรับ properties พื้นฐานด้วย เช่น message, name และ ควรจะมี stack ด้วย แต่มันก็อาจจะมี properties ของมันเองด้วย เช่น HttpError ก็จะมี statusCode property เป็นของตัวเองกับค่า 404 หรือ 403 หรือ 500
JavaScript อนุญาตให้เราใช้ throw กับ argument อะไรก็ได้ ดังนั้น โดยเทคนิคแล้ว เราสามารถสร้าง Class ของเราเองได้โดยไม่ต้องใช้พื้นฐานของ Error แต่ถ้าเราใช้พื้นฐานของ Error ดังนั้นมันจะใช้ obj instanceof Error ในการระบุชนิดของ Error ได้ ดังนั้นเรามาใช้มันดีกว่า
เช่นใน แอปพลิเคชัน ของเรา เราใช้ HttpTimeoutError ที่มันอาจมีพื้นฐานมาจาก HttpError และอื่น ๆ
Extending Error
ยกตัวอย่าง เรามาพิจารณา function readUser(json) ที่ควรอ่าน JSON กับข้อมูล user
นี่คือตัวอย่างของ JSON ที่เราจะใช้
let json = `{ "name": "John", "age": 30 }`;
ภายในของ JSON.parse ถ้าเรารับ JSON ที่ไม่สมบูรณ์มา ดังนั้นมันจะโยน Error SyntaxError แต่ถ้า JSON มาถูกรูปแบบ นั่นไม่ได้หมายความว่า ข้อมูลมันจะถูก บางทีมันอาจจะมีค่าที่หายไป เช่น มันอาจไม่มี name และ age properties ซึ่งนั่นจำเป็นต่อ user ของเรา
function readUser ของเราจะไม่ได้แค่อ่าน JSON แต่มันต้องตรวจสอบว่าข้อมูลมันมาถูกต้องหรือไม่ ถ้ามันไม่มี แสดงว่ามันมาผิดรูปแบบ และเมื่อมี Error แบบนี้เกิดขึ้น และไม่ใช่ SyntaxError เพราะว่า JSON มาถูกรูปแบบแล้ว แต่ว่าข้อมูลไม่ถูกต้อง ดังนั้นเราจะเรียกใช้ ValidationError โดยเราจะสร้าง Class ให้มัน และ Error ชนิดนี้ควรจะเก็บข้อมูลเกี่ยวกับ Error ไว้ด้วย
ValidationError ของเราควรมีพื้นฐานมาจาก Error Class
Error เป็น buitl-in แต่เพื่อความเข้าใจ เราจะแสดงโค้ดภายในให้ดู
// The "pseudocode" for the built-in Error class defined by JavaScript itself
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (different names for different built-in error classes)
this.stack = <call stack>; // non-standard, but most environments support it
}
}
ดังนั้น เราลองมาสร้าง ValidationError มันกัน
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}
function test() {
throw new ValidationError("Whoops!");
}
try {
test();
} catch(err) {
alert(err.message); // Whoops!
alert(err.name); // ValidationError
alert(err.stack); // a list of nested calls with line numbers for each
}
ในบรรทัด (1) เราเรียกใช้ parent constructor. JavaScript กำหนดให้มี super ใน child constructor นั่นเป็นสิ่งจำเป็น เพราะว่า parent constructor ก็มี message property เหมือนกัน
parent constructor ก็มี name property เหมือนกัน ดังนั้นในบรรทัด (2) เรากำหนดให้มันเป็นค่าที่เรากำหนด
เรามาใช้ readUser(json) กัน:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
// Usage
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new ValidationError("No field: age");
}
if (!user.name) {
throw new ValidationError("No field: name");
}
return user;
}
// Working example with try..catch
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No field: name
} else if (err instanceof SyntaxError) { // (*)
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // unknown error, rethrow it (**)
}
}
ใน block try..catch ใน code ด้านบนจะรับมือกับ ValidationError และ SyntaxError จาก JSON.parse.
โปรดดูว่าเราใช้ instanceof ในการตรวจสอบว่า Error นั้น เป็นประเภทอะไร
และเราก็สามารถใช้ err.name ในการตรวจสอบได้เหมือนกัน
// ...
// instead of (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...
แต่เอาจริง ๆ เราใช้ instanceof ดีกว่า เพราะในอนาคตถ้าเราต้องการจะใช้ ValidationError เป็นฐานของประเภท Error แยกย่อยอีก เช่น PropertyRequiredError และใช้ instanceof ตรวจสอบ มันจะทำงานได้กับ Class ใหม่
และถ้า catch เจอ Error ที่ไม่รู้จัก มันจะ rethrows ทิ้ง เพราะมันรู้แค่วิธีจัดการแค่กับ 2 ประเภทของ Error
Further inheritance
ValidationError เป็น Class ที่พื้นฐานมาก, หลาย ๆ อย่างอาจเกิดข้อผิดพลาดได้, property อาจขาดหายไป หรือว่าอยู่ผิดรูปแบบ (เช่น ค่า age เป็น string และไม่ใช่ตัวเลข) เรามาสร้าง Class ที่รองรับ Error แบบนี้กัน PropertyRequiredError ใช้สำหรับ property ที่ขาดหายไป มันจะเก็บข้อมูลเพิ่มเติม ที่เกี่ยวกับ property ที่ขาดหายไป
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.name = "PropertyRequiredError";
this.property = property;
}
}
// Usage
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
return user;
}
// Working example with try..catch
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No property: name
alert(err.name); // PropertyRequiredError
alert(err.property); // name
} else if (err instanceof SyntaxError) {
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // unknown error, rethrow it
}
}
Class ใหม่ PropertyRequiredError ใช้งานได้ง่ายมาก เราแค่ต้องส่ง argument เข้าไป new PropertyRequiredError(property) และ สิ่งที่มนุษย์สามารถอ่านได้ message จะถูกสร้างมาจาก Class
โปรดทราบว่า this.name ใน constructor PropertyRequiredError จะต้องกำหนด this.name ทุกครั้ง ซึ่งอาจกลายเป็นเรื่องน่าเบื่อเล็กน้อยในการกำหนด this.name = <class name> ในทุก ๆ custom error class เราสามารถหลีกเลี่ยงปัญหานี้ได้โดยสร้าง "basic error" ของเราเองที่กำหนด this.name = this.constructor.name และ class ที่สร้างต่อจากมัน ก็จะถูกกำหนดชื่อเองโดยอัตโนมัติ
เรามาเรียกใช้ MyError
โค้ดตัวอย่างของ MyError
class MyError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class ValidationError extends MyError { }
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.property = property;
}
}
// name is correct
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
ตอนนี้โค้ดของเราสั้นลงมาก โดยที่เราตัด "this.name = ..." ออกใน constructor
Wrapping exceptions
หน้าที่ของ readUser คือทำหน้าที่ “อ่านข้อมูล user” นั่นอาจทำให้เกิด Error ประเภทอื่น ๆ ได้. ณ ตอนนี้ เรามี SyntaxError และ ValidationError แต่ในอนาคต readUser อาจจะถูกพัฒนาและสร้าง Error ประเภทอื่นมาอีก
ในโค้ดที่เรียก readUser ควรจะรับมือกับ Error พวกนี้ได้ แต่ตอนนี้เราใช้ if หลาย ๆ ตัวใน catch ในการตรวจสอบประเภทของ Error และ rethrow Error ที่ไม่รู้จัก
try {
...
readUser() // the potential error source
...
} catch (err) {
if (err instanceof ValidationError) {
// handle validation errors
} else if (err instanceof SyntaxError) {
// handle syntax errors
} else {
throw err; // unknown error, rethrow it
}
}
ในโค้ดด้านบนเราสามารถเห็น Error ได้ 2 ประเภท แต่มันสามารถมีได้มากกว่านี้
ถ้า readUser สร้าง Error หลาย ๆ รูปแบบล่ะ ดังนั้น เราควรถามตัวเองว่า เราจะต้องมานั่งตรวจสอบ Error แต่ละประเภท จริง ๆ หรอ
คำตอบคือ ไม่ เราแค่ต้องการจะรู้ว่า มีการ “อ่านข้อมูลผิดพลาด” หรือไม่ หรือถ้าจะให้ดีขึ้น ก็อาจมีรายละเอียดบอกเรา แต่แค่ที่เราต้องการจะรู้
เทคนิคที่เราบอกคือ “wrapping exceptions”.
- เราจะสร้าง class
ReadErrorแสดงถึงการอ่านข้อมูลผิดพลาด - function
readUserจะจับการอ่านข้อมูลที่ผิดพลาดที่เกิดขึ้นภายใน แทนที่จะใช้ValidationErrorและSyntaxErrorเราจะใช้ReadErrorแทน ReadErrorObject จะเก็บการอ้างอิงถึง Error เดิมไว้ในcauseproperty
ดังนั้น โค้ดที่เรียกใช้ readUser จะเช็คแค่ ReadError และไม่ใช่การเช็ค Error ทั้งหมด และถ้ามันต้องการจะรู้รายละเอียดมากกว่านี้ มันจะเช็ค cause property
โค้ดที่เขียน ReadError และใช้กับ readUser และ try..catch:
class ReadError extends Error {
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = 'ReadError';
}
}
class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }
function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
}
function readUser(json) {
let user;
try {
user = JSON.parse(json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError("Syntax Error", err);
} else {
throw err;
}
}
try {
validateUser(user);
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError("Validation Error", err);
} else {
throw err;
}
}
}
try {
readUser('{bad json}');
} catch (e) {
if (e instanceof ReadError) {
alert(e);
// Original error: SyntaxError: Unexpected token b in JSON at position 1
alert("Original error: " + e.cause);
} else {
throw e;
}
}
ในโค้ดด้านบน readUser ทำงานเหมือนที่เรากล่าวไป มันจับ syntax และ validation Error และโยน ReadError แทน
ดังนั้นโค้ดภายนอกจะตรวจสอบโดย instanceof ReadError แทนที่จะเป็นการตรวจสอบ Error ทุกกรณี
วิธีนี้เรียกว่า “wrapping exceptions” เพราะว่าเราจะเอา Error ที่เราสามารถคาดได้ว่าจะเกิดและ “wrap” มันเข้า ReadError นั่นสามารถใช้งานได้ง่ายกว่า และใช้มากในการเขียนโปรแกรมแบบ OOP
Summary
• เราสามารถใช้ Error เป็นพื้นฐาน และ class ที่สร้างขึ้นมาสำหรับ error อื่น ๆ ได้ตามปกติ เพียงแค่ต้องระวังเรื่อง property name และอย่าลืมเรียกใช้ super
• เราสามารถใช้ instanceof เพื่อตรวจสอบ error ประเภทต่าง ๆ ได้ ซึ่งใช้กับตัวที่เป็นตัวสืบทอดได้ด้วย แต่บางครั้งเราอาจได้รับ Error object มาจาก 3rd-party library และ อาจเป็นการยากในการเข้าถึง class ของมัน ในการเข้าถึง class ของมัน ในกรณีนี้เราสามารถใช้ property name ในการตรวจสอบได้
• การ Wrapping exceptions เป็นเทคนิคที่ใช้กันอย่างแพร่หลาย: function จะรับมือกับ Error ที่สามารถคาดเดาได้ และสร้าง errors อันใหม่ขึ้นมาแทนที่จะใช้ errors อันเก่า เพราะว่า บางครั้ง Error อันเก่านั้น อาจกลายเป็น properties ของ object นั้นได้ เช่น err.cause ในตัวอย่างข้างต้น. แต่ไม่จำเป็นต้องเป็นเช่นนั้นเสมอไป