Promise Chains สามารถจัดการข้อผิดพลาดได้ดี เมื่อสัญญาปฏิเสธ ตัวควบคุมจะข้ามไปยังตัวจัดการการปฏิเสธที่ใกล้ที่สุด นั่นสะดวกมากในทางปฏิบัติ
ตัวอย่างเช่น ในโค้ดด้านล่าง URL ที่จะ fetch
ไม่ถูกต้อง (ไม่มีไซต์ดังกล่าว) และ .catch
จัดการข้อผิดพลาด:
fetch('https://no-like-server.blabla') // ปฏิเสธ .then(response => response.json()) .catch(err => alert(err)) // TypeError: ไม่สามารถดึงข้อมูลได้ (ข้อความอาจแตกต่างกันไป)
อย่างที่คุณเห็น .catch
catch ไม่จำเป็นต้องเกิดขึ้นทันที อาจปรากฏหลังจากหนึ่งหรืออาจจะหลาย . .then
หรือบางที ทุกอย่างเรียบร้อยดีกับไซต์ แต่การตอบสนองไม่ใช่ JSON ที่ถูกต้อง วิธีที่ง่ายที่สุดในการตรวจจับข้อผิดพลาดทั้งหมดคือการผนวก .catch
catch ต่อท้ายห่วงโซ่:
ดึงข้อมูล ('https://javascript.info/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) .then(githubUser => new Promise((แก้ไข, ปฏิเสธ) => { ให้ img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "สัญญา-อวตาร-ตัวอย่าง"; document.body.append(img); setTimeout(() => { img.remove(); แก้ไข (githubUser); }, 3000); - .catch(ข้อผิดพลาด => การแจ้งเตือน(error.message));
โดยปกติแล้ว .catch
ดังกล่าวจะไม่ทริกเกอร์เลย แต่หากสัญญาใด ๆ ข้างต้นปฏิเสธ (ปัญหาเครือข่ายหรือ json ที่ไม่ถูกต้องหรืออะไรก็ตาม) มันก็จะจับได้
รหัสของผู้ดำเนินการตามสัญญาและผู้จัดการสัญญามี " try..catch
ที่มองไม่เห็น..จับ" อยู่รอบๆ หากมีข้อยกเว้นเกิดขึ้น จะถูกจับและถือเป็นการปฏิเสธ
ตัวอย่างเช่น รหัสนี้:
สัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { โยนข้อผิดพลาดใหม่ ("อ๊ะ!"); }).จับ(แจ้งเตือน); // ข้อผิดพลาด: อ๊ะ!
…ทำงานเหมือนกับสิ่งนี้ทุกประการ:
สัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { ปฏิเสธ(ข้อผิดพลาดใหม่("อ๊ะ!")); }).จับ(แจ้งเตือน); // ข้อผิดพลาด: อ๊ะ!
“ try..catch
ไม่เห็น” รอบๆ ตัวดำเนินการจะจับข้อผิดพลาดโดยอัตโนมัติและเปลี่ยนให้เป็นสัญญาที่ถูกปฏิเสธ
สิ่งนี้เกิดขึ้นไม่เพียงแต่ในฟังก์ชันตัวดำเนินการเท่านั้น แต่ยังเกิดขึ้นในตัวจัดการด้วยเช่นกัน หากเรา throw
ตัวจัดการ .then
ลงไป นั่นหมายถึงสัญญาที่ถูกปฏิเสธ ดังนั้นตัวควบคุมจึงข้ามไปยังตัวจัดการข้อผิดพลาดที่ใกล้ที่สุด
นี่คือตัวอย่าง:
สัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { แก้ไข ("ตกลง"); }).แล้ว((ผลลัพธ์) => { โยนข้อผิดพลาดใหม่ ("อ๊ะ!"); //ปฏิเสธคำสัญญา }).จับ(แจ้งเตือน); // ข้อผิดพลาด: อ๊ะ!
สิ่งนี้เกิดขึ้นกับข้อผิดพลาดทั้งหมด ไม่ใช่แค่ข้อผิดพลาดที่เกิดจากคำสั่ง throw
ตัวอย่างเช่น ข้อผิดพลาดในการเขียนโปรแกรม:
สัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { แก้ไข ("ตกลง"); }).แล้ว((ผลลัพธ์) => { บลาบลา(); // ไม่มีฟังก์ชันดังกล่าว }).จับ(แจ้งเตือน); // ReferenceError: blabla ไม่ได้ถูกกำหนดไว้
สุดท้าย .catch
ไม่เพียงตรวจจับการปฏิเสธอย่างชัดเจน แต่ยังรวมถึงข้อผิดพลาดโดยไม่ตั้งใจในตัวจัดการด้านบนด้วย
อย่างที่เราสังเกตเห็นแล้วว่า .catch
ที่ส่วนท้ายของ chain นั้นคล้ายกับ try..catch
เราอาจมีตัวจัดการ .then
ได้มากเท่าที่เราต้องการ จากนั้นใช้ .catch
ตัวเดียวที่ส่วนท้ายเพื่อจัดการกับข้อผิดพลาดทั้งหมด
ในการ try..catch
เป็นประจำ เราสามารถวิเคราะห์ข้อผิดพลาดและอาจโยนใหม่อีกครั้งหากไม่สามารถจัดการได้ สิ่งเดียวกันนี้เป็นไปได้สำหรับคำสัญญา
หากเรา throw
เข้าไป .catch
catch การควบคุมจะไปที่ตัวจัดการข้อผิดพลาดถัดไปที่ใกล้เคียงที่สุด และถ้าเราจัดการข้อผิดพลาดและเสร็จสิ้นตามปกติ มันก็จะดำเนินต่อไปยังตัวจัดการที่ใกล้เคียงที่สุดที่ประสบความสำเร็จ . .then
ในตัวอย่างด้านล่าง .catch
catch จัดการข้อผิดพลาดได้สำเร็จ:
// การดำเนินการ: catch -> จากนั้น สัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { โยนข้อผิดพลาดใหม่ ("อ๊ะ!"); }).catch(ฟังก์ชั่น(ข้อผิดพลาด) { alert("แก้ไขข้อผิดพลาดแล้ว ดำเนินการต่อตามปกติ"); }).then(() => alert("ตัวจัดการที่ประสบความสำเร็จถัดไปรัน"));
ที่นี่บล็อก .catch
เสร็จสิ้นตามปกติ ดังนั้นตัวจัดการ .then
ตัวถัดไปที่ประสบความสำเร็จจึงถูกเรียกว่า
ในตัวอย่างด้านล่าง เราจะเห็นอีกสถานการณ์หนึ่งด้วย . .catch
ตัวจัดการ (*)
จับข้อผิดพลาดและไม่สามารถจัดการได้ (เช่น รู้วิธีจัดการ URIError
เท่านั้น) ดังนั้นจึงส่งอีกครั้ง:
// การประหารชีวิต: catch -> catch สัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { โยนข้อผิดพลาดใหม่ ("อ๊ะ!"); }).catch(ฟังก์ชั่น(ข้อผิดพลาด) { // (*) ถ้า (อินสแตนซ์ข้อผิดพลาดของ URIError) { //จัดการเลย } อื่น { alert("ไม่สามารถจัดการข้อผิดพลาดดังกล่าวได้"); ข้อผิดพลาดในการโยน; // การโยนข้อผิดพลาดนี้หรือข้อผิดพลาดอื่นจะข้ามไปยังการจับครั้งถัดไป - }).แล้ว(ฟังก์ชั่น() { /* ไม่ทำงานที่นี่ */ }).catch(ข้อผิดพลาด => { // (**) alert(`เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ: ${error}`); // อย่าส่งคืนสิ่งใด => การดำเนินการเป็นไปตามปกติ -
การดำเนินการจะกระโดดจาก .catch
แรก (*)
ไปยังอันถัดไป (**)
ลงมาในห่วงโซ่
จะเกิดอะไรขึ้นเมื่อข้อผิดพลาดไม่ได้รับการจัดการ? ตัวอย่างเช่น เราลืมเติม .catch
catch ต่อท้าย chain ดังตัวอย่างนี้:
สัญญาใหม่ (ฟังก์ชั่น () { noSuchFunction(); // เกิดข้อผิดพลาดที่นี่ (ไม่มีฟังก์ชันดังกล่าว) - .แล้ว(() => { // ผู้จัดการสัญญาที่ประสบความสำเร็จ หนึ่งคนขึ้นไป - // โดยไม่ต้อง .catch ในตอนท้าย!
ในกรณีที่มีข้อผิดพลาด สัญญาจะถูกปฏิเสธ และการดำเนินการควรข้ามไปยังตัวจัดการการปฏิเสธที่ใกล้ที่สุด แต่ไม่มีเลย ดังนั้นข้อผิดพลาดจึง "ติดอยู่" ไม่มีรหัสที่จะจัดการมัน
ในทางปฏิบัติ เช่นเดียวกับข้อผิดพลาดทั่วไปในโค้ดที่ไม่สามารถจัดการได้ นั่นหมายความว่ามีบางอย่างผิดพลาดร้ายแรง
จะเกิดอะไรขึ้นเมื่อมีข้อผิดพลาดปกติเกิดขึ้นและ try..catch
ตรวจไม่พบ สคริปต์เสียชีวิตพร้อมกับข้อความในคอนโซล สิ่งที่คล้ายกันนี้เกิดขึ้นกับการปฏิเสธคำสัญญาที่ไม่สามารถจัดการได้
เอ็นจิ้น JavaScript ติดตามการปฏิเสธดังกล่าวและสร้างข้อผิดพลาดส่วนกลางในกรณีนั้น คุณสามารถดูได้ในคอนโซลหากคุณเรียกใช้ตัวอย่างด้านบน
ในเบราว์เซอร์เราสามารถตรวจจับข้อผิดพลาดดังกล่าวได้โดยใช้เหตุการณ์ unhandledrejection
:
window.addEventListener ('การปฏิเสธที่ไม่สามารถจัดการได้' ฟังก์ชัน (เหตุการณ์) { // ออบเจ็กต์เหตุการณ์มีคุณสมบัติพิเศษสองประการ: การแจ้งเตือน (เหตุการณ์สัญญา); // [วัตถุสัญญา] - คำสัญญาที่ทำให้เกิดข้อผิดพลาด การแจ้งเตือน (เหตุการณ์เหตุผล); // ข้อผิดพลาด: อ๊ะ! - วัตถุข้อผิดพลาดที่ไม่สามารถจัดการได้ - สัญญาใหม่ (ฟังก์ชั่น () { โยนข้อผิดพลาดใหม่ ("อ๊ะ!"); - // ไม่มีการจับเพื่อจัดการข้อผิดพลาด
เหตุการณ์นี้เป็นส่วนหนึ่งของมาตรฐาน HTML
หากมีข้อผิดพลาดเกิดขึ้น และไม่มี .catch
ตัวจัดการการปฏิเสธ unhandledrejection
จะทริกเกอร์ และรับออบเจ็กต์ event
พร้อมข้อมูลเกี่ยวกับข้อผิดพลาด เพื่อให้เราสามารถดำเนินการบางอย่างได้
โดยปกติแล้วข้อผิดพลาดดังกล่าวจะไม่สามารถกู้คืนได้ ดังนั้นวิธีที่ดีที่สุดของเราคือการแจ้งให้ผู้ใช้ทราบเกี่ยวกับปัญหาและอาจรายงานเหตุการณ์ดังกล่าวไปยังเซิร์ฟเวอร์
ในสภาพแวดล้อมที่ไม่ใช่เบราว์เซอร์ เช่น Node.js มีวิธีอื่นๆ ในการติดตามข้อผิดพลาดที่ไม่สามารถจัดการได้
.catch
จัดการข้อผิดพลาดตามสัญญาทุกประเภท: ไม่ว่าจะเป็นการ reject()
หรือข้อผิดพลาดที่เกิดขึ้นในตัวจัดการ
.then
ยังจับข้อผิดพลาดในลักษณะเดียวกัน หากได้รับอาร์กิวเมนต์ที่สอง (ซึ่งเป็นตัวจัดการข้อผิดพลาด)
เราควรวาง .catch
ในตำแหน่งที่เราต้องการจัดการกับข้อผิดพลาดและรู้วิธีจัดการกับข้อผิดพลาดเหล่านั้น ตัวจัดการควรวิเคราะห์ข้อผิดพลาด (ความช่วยเหลือเกี่ยวกับคลาสข้อผิดพลาดแบบกำหนดเอง) และโยนข้อผิดพลาดที่ไม่รู้จักขึ้นมาใหม่ (อาจเป็นข้อผิดพลาดในการเขียนโปรแกรม)
เป็นเรื่องปกติที่จะไม่ใช้ .catch
เลย หากไม่มีวิธีกู้คืนจากข้อผิดพลาด
ไม่ว่าในกรณีใด เราควรมีตัวจัดการเหตุการณ์ unhandledrejection
(สำหรับเบราว์เซอร์ และแอนะล็อกสำหรับสภาพแวดล้อมอื่นๆ) เพื่อติดตามข้อผิดพลาดที่ไม่สามารถจัดการได้ และแจ้งให้ผู้ใช้ (และอาจเป็นเซิร์ฟเวอร์ของเรา) เกี่ยวกับข้อผิดพลาดเหล่านั้น เพื่อให้แอปของเราไม่ "เพิ่งตาย"
คุณคิดอย่างไร? .catch
จะทริกเกอร์หรือไม่ อธิบายคำตอบของคุณ
สัญญาใหม่ (ฟังก์ชั่น (แก้ไข, ปฏิเสธ) { setTimeout(() => { โยนข้อผิดพลาดใหม่ ("อ๊ะ!"); }, 1,000); }).จับ(แจ้งเตือน);
คำตอบคือ: ไม่ มันจะไม่ :
สัญญาใหม่ (ฟังก์ชั่น (แก้ไข, ปฏิเสธ) { setTimeout(() => { โยนข้อผิดพลาดใหม่ ("อ๊ะ!"); }, 1,000); }).จับ(แจ้งเตือน);
ตามที่กล่าวไว้ในบท มีคำว่า “implicit try..catch
” อยู่รอบๆ โค้ดฟังก์ชัน ดังนั้นข้อผิดพลาดแบบซิงโครนัสทั้งหมดจึงได้รับการจัดการ
แต่ที่นี่ข้อผิดพลาดไม่ได้ถูกสร้างขึ้นในขณะที่ผู้ดำเนินการกำลังทำงานอยู่ แต่จะเกิดขึ้นในภายหลัง คำสัญญาจึงไม่สามารถจัดการได้