การจัดการข้อผิดพลาดที่ดีขึ้นใน NodeJS ด้วยคลาสข้อผิดพลาด
เผยแพร่แล้ว: 2022-03-10error
และวิธีใช้งานเพื่อการจัดการข้อผิดพลาดในแอปพลิเคชันของคุณให้ดีขึ้นและมีประสิทธิภาพมากขึ้นการจัดการข้อผิดพลาดเป็นส่วนหนึ่งของการพัฒนาซอฟต์แวร์ที่ไม่ได้รับความสนใจเท่าที่ควร อย่างไรก็ตาม การสร้างแอปพลิเคชันที่มีประสิทธิภาพจำเป็นต้องจัดการกับข้อผิดพลาดอย่างเหมาะสม
คุณสามารถเข้าถึง NodeJS ได้โดยไม่ต้องจัดการกับข้อผิดพลาดอย่างเหมาะสม แต่เนื่องจาก NodeJS มีลักษณะแบบอะซิงโครนัส การจัดการที่ไม่เหมาะสมหรือข้อผิดพลาดอาจทำให้คุณเจ็บปวดได้ในไม่ช้า โดยเฉพาะอย่างยิ่งเมื่อทำการดีบั๊กแอปพลิเคชัน
ก่อนที่เราจะดำเนินการต่อ ฉันต้องการจะชี้ให้เห็นถึงประเภทของข้อผิดพลาดที่เราจะพูดคุยถึงวิธีการใช้คลาสข้อผิดพลาด
ข้อผิดพลาดในการปฏิบัติงาน
นี่เป็นข้อผิดพลาดที่พบระหว่างรันไทม์ของโปรแกรม ข้อผิดพลาดในการปฏิบัติงานไม่ใช่จุดบกพร่อง และสามารถเกิดขึ้นได้เป็นครั้งคราวเนื่องจากปัจจัยภายนอกอย่างใดอย่างหนึ่งหรือหลายอย่างร่วมกัน เช่น เซิร์ฟเวอร์ฐานข้อมูลหมดเวลา หรือผู้ใช้ตัดสินใจที่จะพยายามฉีด SQL โดยการป้อนแบบสอบถาม SQL ในช่องป้อนข้อมูล
ด้านล่างนี้เป็นตัวอย่างเพิ่มเติมของข้อผิดพลาดในการปฏิบัติงาน:
- ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ฐานข้อมูล
- อินพุตไม่ถูกต้องโดยผู้ใช้ (เซิร์ฟเวอร์ตอบสนองด้วยรหัสตอบกลับ
400
) - หมดเวลาการร้องขอ;
- ไม่พบทรัพยากร (เซิร์ฟเวอร์ตอบกลับด้วยรหัสตอบกลับ 404);
- เซิร์ฟเวอร์กลับมาพร้อมการตอบสนอง
500
นอกจากนี้ยังควรค่าแก่การสังเกตเพื่อหารือสั้น ๆ เกี่ยวกับข้อผิดพลาดในการปฏิบัติงาน
ข้อผิดพลาดของโปรแกรมเมอร์
สิ่งเหล่านี้เป็นข้อบกพร่องในโปรแกรมซึ่งสามารถแก้ไขได้โดยการเปลี่ยนรหัส ไม่สามารถจัดการข้อผิดพลาดประเภทนี้ได้เนื่องจากเกิดขึ้นจากการที่โค้ดใช้งานไม่ได้ ตัวอย่างของข้อผิดพลาดเหล่านี้คือ:
- พยายามอ่านคุณสมบัติบนวัตถุที่ไม่ได้กำหนดไว้
const user = { firstName: 'Kelvin', lastName: 'Omereshone', } console.log(user.fullName) // throws 'undefined' because the property fullName is not defined
- เรียกหรือเรียกฟังก์ชันแบบอะซิงโครนัสโดยไม่ต้องโทรกลับ
- ส่งผ่านสตริงที่คาดหวังตัวเลข
บทความนี้เกี่ยวกับ การจัดการข้อผิดพลาดในการดำเนินงาน ใน NodeJS การจัดการข้อผิดพลาดใน NodeJS แตกต่างอย่างมากจากการจัดการข้อผิดพลาดในภาษาอื่นๆ นี่เป็นเพราะลักษณะอะซิงโครนัสของ JavaScript และความเปิดกว้างของ JavaScript ที่มีข้อผิดพลาด ให้ฉันอธิบาย:
ใน JavaScript อินสแตนซ์ของคลาส error
ไม่ใช่สิ่งเดียวที่คุณสามารถโยนได้ คุณสามารถโยนข้อมูลประเภทใด ๆ ได้อย่างแท้จริงซึ่งการเปิดกว้างนี้ไม่ได้รับอนุญาตโดยภาษาอื่น
ตัวอย่างเช่น นักพัฒนา JavaScript อาจตัดสินใจที่จะใส่ตัวเลขแทนที่จะเป็นอินสแตนซ์อ็อบเจกต์ข้อผิดพลาด เช่น:
// bad throw 'Whoops :)'; // good throw new Error('Whoops :)')
คุณอาจไม่เห็นปัญหาในการโยนข้อมูลประเภทอื่น แต่การทำเช่นนั้นจะทำให้การดีบักยากขึ้น เนื่องจากคุณจะไม่ได้รับการติดตามสแต็กและคุณสมบัติอื่นๆ ที่อ็อบเจ็กต์ Error เปิดเผยซึ่งจำเป็นสำหรับการดีบัก
มาดูรูปแบบที่ไม่ถูกต้องในการจัดการข้อผิดพลาด ก่อนที่จะพิจารณารูปแบบคลาสข้อผิดพลาดและวิธีที่ดีกว่ามากในการจัดการข้อผิดพลาดใน NodeJS
รูปแบบการจัดการข้อผิดพลาดที่ไม่ดี #1: การใช้การโทรกลับอย่างไม่ถูกต้อง
สถานการณ์ในโลกแห่งความเป็นจริง : รหัสของคุณขึ้นอยู่กับ API ภายนอกที่ต้องการการเรียกกลับเพื่อให้ได้ผลลัพธ์ที่คุณคาดหวังให้ส่งคืน
ลองใช้ข้อมูลโค้ดด้านล่าง:
'use strict'; const fs = require('fs'); const write = function () { fs.mkdir('./writeFolder'); fs.writeFile('./writeFolder/foobar.txt', 'Hello World'); } write();
โค้ดด้านบนนั้นถูกต้องตามกฎหมาย จนถึง NodeJS 8 ขึ้นไป และนักพัฒนาเพียงแค่เริ่มและลืมคำสั่ง ซึ่งหมายความว่านักพัฒนาไม่จำเป็นต้องให้การเรียกกลับการเรียกใช้ฟังก์ชันดังกล่าว ดังนั้นจึงสามารถละเว้นการจัดการข้อผิดพลาดได้ จะเกิดอะไรขึ้นเมื่อไม่ได้สร้าง writeFolder
จะไม่มีการโทรไปยัง writeFile
และเราจะไม่รู้อะไรเกี่ยวกับมันเลย ซึ่งอาจส่งผลให้เกิดสภาวะการแข่งขันเนื่องจากคำสั่งแรกอาจยังไม่เสร็จสิ้นเมื่อคำสั่งที่สองเริ่มต้นอีกครั้ง คุณคงไม่รู้
มาเริ่มแก้ปัญหานี้ด้วยการแก้สภาพการแข่งขันกัน เราจะทำได้โดยให้เรียกกลับไปยังคำสั่งแรก mkdir
เพื่อให้แน่ใจว่าไดเร็กทอรีมีอยู่จริงก่อนที่จะเขียนไปยังคำสั่งที่สอง ดังนั้นโค้ดของเราจะมีลักษณะดังนี้:
'use strict'; const fs = require('fs'); const write = function () { fs.mkdir('./writeFolder', () => { fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); }); } write();
แม้ว่าเราจะแก้ไขสภาพการแข่งขันแล้ว เรายังดำเนินการไม่เสร็จ รหัสของเรายังคงมีปัญหาเพราะแม้ว่าเราจะใช้การเรียกกลับสำหรับคำสั่งแรก แต่เราไม่มีทางรู้ได้ว่าโฟลเดอร์ writeFolder
ถูกสร้างขึ้นหรือไม่ หากไม่ได้สร้างโฟลเดอร์ การโทรครั้งที่สองจะล้มเหลวอีกครั้ง แต่ถึงกระนั้น เราก็เพิกเฉยต่อข้อผิดพลาดอีกครั้ง เราแก้ปัญหานี้โดย...
ข้อผิดพลาดในการจัดการกับการโทรกลับ
เพื่อจัดการกับข้อผิดพลาดอย่างถูกต้องด้วยการโทรกลับ คุณต้องแน่ใจว่าคุณใช้แนวทางที่ข้อผิดพลาดเป็นอันดับแรกเสมอ สิ่งนี้หมายความว่าคุณควรตรวจสอบก่อนว่ามีข้อผิดพลาดที่ส่งคืนจากฟังก์ชันหรือไม่ก่อนที่จะใช้ข้อมูลใด ๆ (ถ้ามี) ที่ส่งคืน ลองดูวิธีที่ผิดในการทำเช่นนี้:
'use strict'; // Wrong const fs = require('fs'); const write = function (callback) { fs.mkdir('./writeFolder', (err, data) => { if (data) fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); else callback(err) }); } write(console.log);
รูปแบบข้างต้นไม่ถูกต้องเนื่องจากบางครั้ง API ที่คุณกำลังเรียกใช้อาจไม่ส่งคืนค่าใดๆ หรืออาจคืนค่าที่ผิดพลาดเป็นค่าตอบแทนที่ถูกต้อง นี้จะทำให้คุณจบลงในกรณีข้อผิดพลาดแม้ว่าคุณอาจเห็นได้ชัดว่ามีการเรียกใช้ฟังก์ชันหรือ API ที่ประสบความสำเร็จ
รูปแบบข้างต้นก็ไม่ดีเช่นกัน เนื่องจากการใช้งานจะกินข้อผิดพลาดของคุณ (ข้อผิดพลาดของคุณจะไม่ถูกเรียกแม้ว่าอาจเกิดขึ้น) คุณจะไม่รู้ว่าเกิดอะไรขึ้นในโค้ดของคุณอันเป็นผลมาจากรูปแบบการจัดการข้อผิดพลาดประเภทนี้ ดังนั้นวิธีที่ถูกต้องสำหรับโค้ดด้านบนคือ:
'use strict'; // Right const fs = require('fs'); const write = function (callback) { fs.mkdir('./writeFolder', (err, data) => { if (err) return callback(err) fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); }); } write(console.log);
รูปแบบการจัดการข้อผิดพลาดที่ผิด #2: การใช้คำสัญญาอย่างไม่ถูกต้อง
สถานการณ์ในโลกแห่งความเป็นจริง : ดังนั้นคุณจึงค้นพบ Promises และคุณคิดว่าสิ่งเหล่านี้ดีกว่าการเรียกกลับเนื่องจากปัญหาการโทรกลับ และคุณตัดสินใจให้สัญญากับ API ภายนอกบางตัวที่ฐานโค้ดของคุณต้องพึ่งพา หรือคุณกำลังใช้สัญญาจาก API ภายนอกหรือ API ของเบราว์เซอร์เช่นฟังก์ชัน fetch()
วันนี้เราไม่ได้ใช้การเรียกกลับในฐานรหัส NodeJS ของเราจริงๆ เราใช้สัญญา ลองนำโค้ดตัวอย่างของเราไปใช้ใหม่ด้วยสัญญา:
'use strict'; const fs = require('fs').promises; const write = function () { return fs.mkdir('./writeFolder').then(() => { fs.writeFile('./writeFolder/foobar.txt', 'Hello world!') }).catch((err) => { // catch all potential errors console.error(err) }) }
มาใส่โค้ดด้านบนนี้ภายใต้กล้องจุลทรรศน์กัน — เราจะเห็นว่าเรากำลังแยกสัญญา fs.mkdir
ออกเป็นสายสัญญาอื่น (การเรียกไปที่ fs.writeFile) โดยไม่ต้องจัดการกับการเรียกตามคำสัญญานั้น คุณอาจคิดว่าวิธีที่ดีกว่าที่จะทำคือ:
'use strict'; const fs = require('fs').promises; const write = function () { return fs.mkdir('./writeFolder').then(() => { fs.writeFile('./writeFolder/foobar.txt', 'Hello world!').then(() => { // do something }).catch((err) => { console.error(err); }) }).catch((err) => { // catch all potential errors console.error(err) }) }
แต่ข้างต้นจะไม่ปรับขนาด นี่เป็นเพราะถ้าเรามีสายสัญญาที่จะโทรมากขึ้น เราจะจบลงด้วยบางสิ่งที่คล้ายกับ callback hell ซึ่งสัญญาว่าจะแก้ไข ซึ่งหมายความว่าโค้ดของเราจะเยื้องไปทางขวา เราจะมีสัญญานรกในมือของเรา
สัญญา API ที่ใช้การโทรกลับ
ส่วนใหญ่คุณต้องการให้คำมั่นสัญญากับ API แบบเรียกกลับด้วยตนเองเพื่อจัดการกับข้อผิดพลาดใน API นั้นได้ดียิ่งขึ้น อย่างไรก็ตาม มันไม่ง่ายเลยที่จะทำ มาดูตัวอย่างด้านล่างเพื่ออธิบายว่าทำไม
function doesWillNotAlwaysSettle(arg) { return new Promise((resolve, reject) => { doATask(foo, (err) => { if (err) { return reject(err); } if (arg === true) { resolve('I am Done') } }); }); }
จากข้างต้น หาก arg
ไม่ true
และเราไม่มีข้อผิดพลาดจากการเรียกไปยังฟังก์ชัน doATask
สัญญานี้ก็จะแฮงเอาท์ซึ่งเป็นหน่วยความจำรั่วในแอปพลิเคชันของคุณ
ข้อผิดพลาดในการซิงค์ที่ถูกกลืนหายไปในสัญญา
การใช้ Promise Constructor มีปัญหาหลายประการ หนึ่งในปัญหาเหล่านี้คือ ทันทีที่ได้รับการแก้ไขหรือปฏิเสธจะไม่สามารถรับสถานะอื่นได้ นี่เป็นเพราะคำสัญญาสามารถรับได้เพียงสถานะเดียวเท่านั้น - ไม่ว่าจะอยู่ระหว่างดำเนินการหรือได้รับการแก้ไข/ปฏิเสธ ซึ่งหมายความว่าเราสามารถมีจุดบอดในคำสัญญาของเราได้ ลองดูสิ่งนี้ในรหัส:
function deadZonePromise(arg) { return new Promise((resolve, reject) => { doATask(foo, (err) => { resolve('I'm all Done'); throw new Error('I am never reached') // Dead Zone }); }); }
จากข้างต้น เราจะเห็นทันทีที่สัญญาได้รับการแก้ไข บรรทัดถัดไปคือโซนอันตราย และจะไม่มีวันไปถึง ซึ่งหมายความว่าการจัดการข้อผิดพลาดแบบซิงโครนัสที่ทำตามสัญญาของคุณจะถูกกลืนเข้าไปและจะไม่ถูกโยนทิ้ง
ตัวอย่างในโลกแห่งความเป็นจริง
ตัวอย่างข้างต้นช่วยอธิบายรูปแบบการจัดการข้อผิดพลาดที่ไม่ดี ลองมาดูปัญหาที่คุณอาจพบในชีวิตจริง
ตัวอย่างในโลกแห่งความจริง #1 — การเปลี่ยนข้อผิดพลาดเป็นสตริง
สถานการณ์จำลอง : คุณตัดสินใจว่าข้อผิดพลาดที่ส่งคืนจาก API นั้นไม่ดีพอสำหรับคุณ คุณจึงตัดสินใจเพิ่มข้อความของคุณเองลงไป
'use strict'; function readTemplate() { return new Promise(() => { databaseGet('query', function(err, data) { if (err) { reject('Template not found. Error: ', + err); } else { resolve(data); } }); }); } readTemplate();
ลองดูสิ่งที่ผิดปกติกับโค้ดด้านบนนี้ จากด้านบน เราจะเห็นว่านักพัฒนาพยายามปรับปรุงข้อผิดพลาดที่เกิดจาก databaseGet
รับ API โดยการต่อข้อผิดพลาดที่ส่งคืนมากับสตริง “ไม่พบเทมเพลต” วิธีการนี้มีข้อเสียมากมาย เนื่องจากเมื่อทำการต่อข้อมูลเสร็จแล้ว ผู้พัฒนาจะรัน toString
โดยปริยายบนอ็อบเจกต์ข้อผิดพลาดที่ส่งคืน วิธีนี้ทำให้เขาสูญเสียข้อมูลเพิ่มเติมที่ส่งคืนโดยข้อผิดพลาด (บอกลาการติดตามสแต็ก) ดังนั้นสิ่งที่นักพัฒนามีในตอนนี้จึงเป็นเพียงสตริงที่ไม่มีประโยชน์เมื่อทำการดีบั๊ก
วิธีที่ดีกว่าคือการรักษาข้อผิดพลาดตามที่เป็นอยู่หรือรวมไว้ในข้อผิดพลาดอื่นที่คุณได้สร้างและแนบข้อผิดพลาดที่ส่งมาจากฐานข้อมูลรับการเรียกเป็นคุณสมบัติ
ตัวอย่างในโลกแห่งความเป็นจริง #2: ละเว้นข้อผิดพลาดโดยสิ้นเชิง
สถานการณ์สมมติ : บางทีเมื่อผู้ใช้ลงทะเบียนในแอปพลิเคชันของคุณ หากเกิดข้อผิดพลาด คุณต้องการเพียงแค่ตรวจจับข้อผิดพลาดและแสดงข้อความที่กำหนดเอง แต่คุณละเลยข้อผิดพลาดที่ถูกจับโดยไม่ได้บันทึกเพื่อจุดประสงค์ในการดีบั๊ก
router.get('/:id', function (req, res, next) { database.getData(req.params.userId) .then(function (data) { if (data.length) { res.status(200).json(data); } else { res.status(404).end(); } }) .catch(() => { log.error('db.rest/get: could not get data: ', req.params.userId); res.status(500).json({error: 'Internal server error'}); }) });
จากด้านบน เราจะเห็นได้ว่าข้อผิดพลาดถูกละเว้นอย่างสมบูรณ์ และรหัสกำลังส่ง 500 ถึงผู้ใช้หากการโทรไปยังฐานข้อมูลล้มเหลว แต่ในความเป็นจริง สาเหตุของความล้มเหลวของฐานข้อมูลอาจเป็นข้อมูลที่ผู้ใช้ส่งมาผิดรูปแบบ ซึ่งเป็นข้อผิดพลาดที่มีรหัสสถานะ 400

ในกรณีข้างต้น เราจะจบลงด้วยความสยองขวัญในการดีบัก เพราะคุณในฐานะนักพัฒนาซอฟต์แวร์จะไม่ทราบว่าเกิดอะไรขึ้น ผู้ใช้จะไม่สามารถให้รายงานที่เหมาะสมได้เนื่องจากข้อผิดพลาดเซิร์ฟเวอร์ภายใน 500 รายการมักถูกส่งออกไป คุณจะต้องเสียเวลาหลายชั่วโมงในการค้นหาปัญหา ซึ่งเท่ากับเสียเวลาและเงินของนายจ้างไปเปล่าๆ
ตัวอย่างในโลกแห่งความเป็นจริง #3: ไม่ยอมรับข้อผิดพลาดที่เกิดจาก API
สถานการณ์จำลอง : มีข้อผิดพลาดเกิดขึ้นจาก API ที่คุณใช้อยู่ แต่คุณไม่ยอมรับข้อผิดพลาดนั้น แต่คุณจัดการและแปลงข้อผิดพลาดในลักษณะที่ทำให้ไม่มีประโยชน์สำหรับการดีบัก
ใช้ตัวอย่างโค้ดต่อไปนี้ด้านล่าง:
async function doThings(input) { try { validate(input); try { await db.create(input); } catch (error) { error.message = `Inner error: ${error.message}` if (error instanceof Klass) { error.isKlass = true; } throw error } } catch (error) { error.message = `Could not do things: ${error.message}`; await rollback(input); throw error; } }
มีหลายอย่างเกิดขึ้นในโค้ดด้านบนที่จะนำไปสู่การแก้จุดบกพร่องสยองขวัญ ลองดู:
- การห่อบล็อกการ
try/catch
: คุณสามารถเห็นได้จากด้านบนว่าเรากำลังห่อบล็อกการtry/catch
ซึ่งเป็นแนวคิดที่แย่มาก ปกติแล้วเราจะพยายามลดการใช้บล็อกtry/catch
เพื่อลดขนาดพื้นผิวที่เราจะต้องจัดการกับข้อผิดพลาดของเรา (คิดว่าเป็นการจัดการข้อผิดพลาด DRY) - เรากำลังจัดการกับข้อความแสดงข้อผิดพลาดเพื่อพยายามปรับปรุง ซึ่งไม่ใช่ความคิดที่ดีเช่นกัน
- เรากำลังตรวจสอบว่าข้อผิดพลาดนั้นเป็นอินสแตนซ์ของประเภท
Klass
หรือไม่ และในกรณีนี้ เรากำลังตั้งค่าคุณสมบัติบูลีนของข้อผิดพลาดisKlass
เป็น truev (แต่หากการตรวจสอบนั้นผ่าน แสดงว่าข้อผิดพลาดนั้นเป็นประเภทKlass
); - เรากำลังย้อนกลับฐานข้อมูลเร็วเกินไป เนื่องจากจากโครงสร้างโค้ด มีแนวโน้มสูงที่เราอาจไม่ได้เข้าถึงฐานข้อมูลเมื่อเกิดข้อผิดพลาด
ด้านล่างนี้เป็นวิธีที่ดีกว่าในการเขียนโค้ดด้านบน:
async function doThings(input) { validate(input); try { await db.create(input); } catch (error) { try { await rollback(); } catch (error) { logger.log('Rollback failed', error, 'input:', input); } throw error; } }
มาวิเคราะห์สิ่งที่เรากำลังทำในตัวอย่างด้านบน:
- เรากำลังใช้บล็อก
try/catch
หนึ่งบล็อก และเฉพาะในบล็อก catch เท่านั้นที่เราใช้บล็อกtry/catch
อื่นเพื่อทำหน้าที่เป็นยามในกรณีที่มีบางอย่างเกิดขึ้นกับฟังก์ชันย้อนกลับนั้น และเรากำลังบันทึกสิ่งนั้น - สุดท้าย เรากำลังโยนข้อผิดพลาดเดิมที่ได้รับ ซึ่งหมายความว่าเราจะไม่สูญเสียข้อความที่รวมอยู่ในข้อผิดพลาดนั้น
การทดสอบ
ส่วนใหญ่เราต้องการทดสอบโค้ดของเรา (ไม่ว่าจะด้วยตนเองหรือโดยอัตโนมัติ) แต่ส่วนใหญ่แล้ว เรากำลังทดสอบแต่สิ่งที่เป็นบวกเท่านั้น สำหรับการทดสอบที่มีประสิทธิภาพ คุณต้องทดสอบหาข้อผิดพลาดและกรณีของขอบด้วย ความประมาทเลินเล่อนี้ทำให้เกิดข้อบกพร่องในการหาทางเข้าสู่การผลิตซึ่งจะทำให้เสียเวลาในการดีบักเพิ่มเติม
เคล็ดลับ : อย่าลืมทดสอบไม่เพียงแต่สิ่งที่เป็นบวก (รับรหัสสถานะ 200 จากปลายทาง) แต่ยังรวมถึงกรณีข้อผิดพลาดทั้งหมดและกรณีขอบทั้งหมดด้วย
ตัวอย่างในโลกแห่งความเป็นจริง #4: การปฏิเสธที่ไม่สามารถจัดการได้
หากคุณเคยใช้คำสัญญามาก่อน คุณอาจพบกับ unhandled rejections
ที่ไม่สามารถจัดการได้
ต่อไปนี้เป็นข้อมูลเบื้องต้นเกี่ยวกับการปฏิเสธที่ไม่สามารถจัดการได้ การปฏิเสธที่ไม่สามารถจัดการได้คือการปฏิเสธสัญญาที่ไม่ได้จัดการ ซึ่งหมายความว่าสัญญาถูกปฏิเสธ แต่รหัสของคุณจะทำงานต่อไป
มาดูตัวอย่างทั่วไปในโลกแห่งความเป็นจริงที่นำไปสู่การปฏิเสธที่ไม่สามารถจัดการได้..
'use strict'; async function foobar() { throw new Error('foobar'); } async function baz() { throw new Error('baz') } (async function doThings() { const a = foobar(); const b = baz(); try { await a; await b; } catch (error) { // ignore all errors! } })();
โค้ดด้านบนในตอนแรกอาจดูเหมือนไม่มีข้อผิดพลาด แต่เมื่อมองใกล้ ๆ เราเริ่มเห็นข้อบกพร่อง ให้ฉันอธิบาย: จะเกิดอะไรขึ้นเมื่อ a
ถูกปฏิเสธ นั่นหมายความว่าจะไม่มีการ await b
และนั่นหมายถึงการปฏิเสธที่ไม่สามารถจัดการได้ วิธีแก้ปัญหาที่เป็นไปได้คือใช้ Promise.all
กับทั้งสองสัญญา ดังนั้นรหัสจะอ่านดังนี้:
'use strict'; async function foobar() { throw new Error('foobar'); } async function baz() { throw new Error('baz') } (async function doThings() { const a = foobar(); const b = baz(); try { await Promise.all([a, b]); } catch (error) { // ignore all errors! } })();
นี่เป็นอีกสถานการณ์หนึ่งในโลกแห่งความเป็นจริงที่จะนำไปสู่ข้อผิดพลาดในการปฏิเสธคำสัญญาที่ไม่สามารถจัดการได้:
'use strict'; async function foobar() { throw new Error('foobar'); } async function doThings() { try { return foobar() } catch { // ignoring errors again ! } } doThings();
หากคุณเรียกใช้ข้อมูลโค้ดข้างต้น คุณจะได้รับการปฏิเสธคำมั่นสัญญาที่ไม่สามารถจัดการได้ และนี่คือสาเหตุ: แม้ว่าจะยังไม่ชัดเจน แต่เรากำลังส่งคืนสัญญา (foobar) ก่อนที่เราจะจัดการกับ try/catch
สิ่งที่เราควรทำคือรอคำสัญญาที่เรากำลังจัดการกับ try/catch
เพื่อให้โค้ดอ่านได้:
'use strict'; async function foobar() { throw new Error('foobar'); } async function doThings() { try { return await foobar() } catch { // ignoring errors again ! } } doThings();
จบเรื่องแย่ๆ
ตอนนี้ คุณได้เห็นรูปแบบการจัดการข้อผิดพลาดที่ไม่ถูกต้อง และการแก้ไขที่เป็นไปได้ ตอนนี้ มาดูรูปแบบคลาสข้อผิดพลาดและวิธีแก้ปัญหาการจัดการข้อผิดพลาดที่ไม่ถูกต้องใน NodeJS กัน
คลาสข้อผิดพลาด
ในรูปแบบนี้ เราจะเริ่มแอปพลิเคชันของเราด้วยคลาส ApplicationError
ด้วยวิธีนี้ เราจึงรู้ว่าข้อผิดพลาดทั้งหมดในแอปพลิเคชันของเราที่เราโยนทิ้งไปโดยชัดแจ้งจะสืบทอดจากข้อผิดพลาดนั้น ดังนั้นเราจะเริ่มต้นด้วยคลาสข้อผิดพลาดต่อไปนี้:
-
ApplicationError
นี่คือบรรพบุรุษของคลาสข้อผิดพลาดอื่น ๆ ทั้งหมด เช่น คลาสข้อผิดพลาดอื่น ๆ ทั้งหมดที่สืบทอดมาจากคลาสนั้น -
DatabaseError
ข้อผิดพลาดใด ๆ ที่เกี่ยวข้องกับการดำเนินการฐานข้อมูลจะสืบทอดมาจากคลาสนี้ -
UserFacingError
ข้อผิดพลาดใด ๆ ที่เกิดขึ้นจากการที่ผู้ใช้โต้ตอบกับแอปพลิเคชันจะสืบทอดมาจากคลาสนี้
นี่คือลักษณะของไฟล์คลาส error
ของเรา:
'use strict'; // Here is the base error classes to extend from class ApplicationError extends Error { get name() { return this.constructor.name; } } class DatabaseError extends ApplicationError { } class UserFacingError extends ApplicationError { } module.exports = { ApplicationError, DatabaseError, UserFacingError }
วิธีนี้ช่วยให้เราแยกแยะข้อผิดพลาดที่เกิดจากแอปพลิเคชันของเราได้ ดังนั้น หากเราต้องการจัดการกับข้อผิดพลาดของคำขอที่ไม่ถูกต้อง (อินพุตของผู้ใช้ไม่ถูกต้อง) หรือข้อผิดพลาดที่ไม่พบ (ไม่พบทรัพยากร) เราสามารถสืบทอดจากคลาสพื้นฐานซึ่งก็คือ UserFacingError
(ดังในโค้ดด้านล่าง)
const { UserFacingError } = require('./baseErrors') class BadRequestError extends UserFacingError { constructor(message, options = {}) { super(message); // You can attach relevant information to the error instance // (eg. the username) for (const [key, value] of Object.entries(options)) { this[key] = value; } } get statusCode() { return 400; } } class NotFoundError extends UserFacingError { constructor(message, options = {}) { super(message); // You can attach relevant information to the error instance // (eg. the username) for (const [key, value] of Object.entries(options)) { this[key] = value; } } get statusCode() { return 404 } } module.exports = { BadRequestError, NotFoundError }
ข้อดีอย่างหนึ่งของแนวทางคลาส error
คือ ถ้าเราโยนข้อผิดพลาดเหล่านี้ออก เช่น NotFoundError
นักพัฒนาทุกคนที่อ่านโค้ดเบสนี้จะสามารถเข้าใจสิ่งที่เกิดขึ้น ณ เวลานี้ (หากพวกเขาอ่านโค้ด ).
คุณจะสามารถส่งผ่านคุณสมบัติต่างๆ เฉพาะสำหรับคลาสข้อผิดพลาดแต่ละคลาสได้เช่นกัน ในระหว่างการสร้างข้อผิดพลาดนั้น
ประโยชน์หลักอีกประการหนึ่งคือคุณสามารถมีคุณสมบัติที่เป็นส่วนหนึ่งของคลาสข้อผิดพลาดเสมอ ตัวอย่างเช่น หากคุณได้รับข้อผิดพลาด UserFacing คุณจะรู้ว่า statusCode เป็นส่วนหนึ่งของคลาสข้อผิดพลาดนี้เสมอ ตอนนี้คุณสามารถใช้โดยตรงใน รหัสในภายหลัง
เคล็ดลับในการใช้คลาสข้อผิดพลาด
- สร้างโมดูลของคุณเอง (อาจเป็นโมดูลส่วนตัว) สำหรับแต่ละคลาสข้อผิดพลาด ด้วยวิธีนี้คุณสามารถนำเข้าสิ่งนั้นในแอปพลิเคชันของคุณและใช้งานได้ทุกที่
- โยนเฉพาะข้อผิดพลาดที่คุณสนใจ (ข้อผิดพลาดที่เป็นอินสแตนซ์ของคลาสข้อผิดพลาดของคุณ) วิธีนี้จะทำให้คุณรู้ว่าคลาสข้อผิดพลาดเป็นแหล่งที่มาของความจริงเพียงแหล่งเดียว และมีข้อมูลทั้งหมดที่จำเป็นในการดีบักแอปพลิเคชันของคุณ
- การมีโมดูลข้อผิดพลาดแบบนามธรรมนั้นมีประโยชน์มากเพราะตอนนี้เราทราบข้อมูลที่จำเป็นทั้งหมดเกี่ยวกับข้อผิดพลาดที่แอปพลิเคชันของเราสามารถโยนทิ้งได้ในที่เดียว
- จัดการข้อผิดพลาดในเลเยอร์ หากคุณจัดการกับข้อผิดพลาดในทุกที่ แสดงว่าคุณมีแนวทางการจัดการข้อผิดพลาดที่ไม่สอดคล้องกันซึ่งยากต่อการติดตาม ตามชั้น ฉันหมายถึงเช่นฐานข้อมูล ชั้น express/fastify/HTTP และอื่นๆ
เรามาดูกันว่าคลาสข้อผิดพลาดมีลักษณะอย่างไรในโค้ด นี่คือตัวอย่างด่วน:
const { DatabaseError } = require('./error') const { NotFoundError } = require('./userFacingErrors') const { UserFacingError } = require('./error') // Express app.get('/:id', async function (req, res, next) { let data try { data = await database.getData(req.params.userId) } catch (err) { return next(err); } if (!data.length) { return next(new NotFoundError('Dataset not found')); } res.status(200).json(data) }) app.use(function (err, req, res, next) { if (err instanceof UserFacingError) { res.sendStatus(err.statusCode); // or res.status(err.statusCode).send(err.errorCode) } else { res.sendStatus(500) } // do your logic logger.error(err, 'Parameters: ', req.params, 'User data: ', req.user) });
จากข้างต้น เรากำลังใช้ประโยชน์จาก Express ที่เปิดเผยตัวจัดการข้อผิดพลาดทั่วโลก ซึ่งช่วยให้คุณจัดการกับข้อผิดพลาดทั้งหมดได้ในที่เดียว คุณสามารถดูการเรียกไปที่ next()
ในตำแหน่งที่เรากำลังจัดการข้อผิดพลาด การเรียกนี้จะส่งต่อข้อผิดพลาดไปยังตัวจัดการซึ่งกำหนดไว้ในส่วน app.use
เนื่องจาก express ไม่รองรับ async/await เราจึงใช้บล็อก try/catch
ดังนั้นจากโค้ดด้านบน เพื่อจัดการกับข้อผิดพลาดของเรา เราเพียงแค่ต้องตรวจสอบว่าข้อผิดพลาดที่เกิดขึ้นนั้นเป็นอินสแตนซ์ UserFacingError
หรือไม่ และเราจะทราบโดยอัตโนมัติว่าจะมี statusCode ในวัตถุข้อผิดพลาด และเราส่งสิ่งนั้นไปยังผู้ใช้ (คุณอาจต้องการ เพื่อให้มีรหัสข้อผิดพลาดเฉพาะซึ่งคุณสามารถส่งผ่านไปยังไคลเอนต์ได้) และนั่นก็ค่อนข้างจะเป็นเช่นนั้น
คุณจะสังเกตเห็นว่าในรูปแบบนี้ ( รูปแบบคลาส error
) ทุกข้อผิดพลาดอื่น ๆ ที่คุณไม่ได้โยนอย่างชัดเจนคือข้อผิดพลาด 500
เนื่องจากเป็นสิ่งที่ไม่คาดคิดซึ่งหมายความว่าคุณไม่ได้โยนข้อผิดพลาดนั้นในแอปพลิเคชันของคุณอย่างชัดเจน ด้วยวิธีนี้ เราจึงสามารถแยกแยะประเภทของข้อผิดพลาดที่เกิดขึ้นในแอปพลิเคชันของเราได้
บทสรุป
การจัดการข้อผิดพลาดที่เหมาะสมในแอปพลิเคชันของคุณสามารถทำให้คุณนอนหลับได้ดีขึ้นในเวลากลางคืนและประหยัดเวลาในการแก้ไขข้อบกพร่อง ต่อไปนี้คือประเด็นสำคัญบางประการที่ควรนำมาจากบทความนี้:
- ใช้คลาสข้อผิดพลาดที่ตั้งค่าไว้สำหรับแอปพลิเคชันของคุณโดยเฉพาะ
- ใช้ตัวจัดการข้อผิดพลาดที่เป็นนามธรรม
- ใช้ async/await เสมอ
- แสดงข้อผิดพลาดอย่างแสดงออก
- ผู้ใช้สัญญาหากจำเป็น
- ส่งคืนสถานะและรหัสข้อผิดพลาดที่เหมาะสม
- ใช้ประโยชน์จากตะขอสัญญา
front-end & UX bits ที่มีประโยชน์ จัดส่งสัปดาห์ละครั้ง
ด้วยเครื่องมือที่จะช่วยให้คุณทำงานให้ลุล่วงได้ดียิ่งขึ้น สมัครและรับ รายการตรวจสอบการออกแบบอินเทอร์เฟซอัจฉริยะของ Vitaly PDF ทางอีเมล
ที่ส่วนหน้าและ UX ได้รับความไว้วางใจจากผู้คนจำนวน 190.000 คน