Promise
Introduction: callbacks
function หลาย ๆ ตัว ถูกกำหนดด้วย host environments ที่จะทำให้เราเขียน asynchronous actions ได้. พูดอีกแบบคือ การกระทำที่เราทำตอนนี้ จะไปจบอีกทีนึง
ตัวอย่างเช่น setTimeout function
และยังมีตัวอย่างในงานจริงของ asynchronous actions เช่น การโหลด script และ modules
ลองมาดูตัวอย่าง loadScript(src) ที่จะโหลด script ตาม src ที่ให้
function loadScript(src) {
// creates a <script> tag and append it to the page
// this causes the script with given src to start loading and run when complete
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
เป็นการแทรก <script src="…"> เข้าไปใน document และ Browser จะเริ่มโหลด script อัตโนมัติ และ execute เมื่อโหลดเสร็จ
เราสามารถเรียกใช้ function ได้แบบนี้
// load and execute the script at the given path
loadScript('/my/script.js');
script นี้ execute แบบ “asynchronously”, โดยมันเริ่มโหลดตอนนี้ แต่จะรันทีหลังเมื่อโหลดเสร็จ
แต่ถ้ามีโค้ดใต้ loadScript โค้ดข้างล่างจะไม่รอมันโหลดเสร็จ
loadScript('/my/script.js');
// the code below loadScript
// doesn't wait for the script loading to finish
// ...
ลองบอกว่าเราต้องการที่ใช้ script หลังจากที่มันโหลดเสร็จ และภายใน script นั้น ประกาศ function ใหม่ไว้ และต้องการที่จะเรียกใช้ function นั้นหลักจากโค้ดบรรทัด loadScript(…) . มันจะไม่สามารถทำงานได้:
loadScript('/my/script.js'); // the script has "function newFunction() {…}"
newFunction(); // no such function!
แน่นอนว่า Browser อาจไม่สามารถโหลด script ได้ทัน. และเรา ก็ไม่สามารถรู้ได้ว่า loadScript จะโหลดเสร็จตอนไหน และเราต้องการที่จะรู้ว่า script นั้น โหลดเสร็จตอนไหน
เรามาเพิ่ม callback function เป็น argument ตัวที่สองของ loadScript และมันควร execute เมื่อ script โหลดเสร็จ
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
onload event ถูกอธิบายไว้ใน Resource loading: onload and onerror, ปกติมันจะทำ function หลังจากที่ script โหลดเสร็จ และถูก execute
และถ้าเราต้องการที่จะเรียกใช้ function เราก็แค่ไปเขียนใน callBack function
loadScript('/my/script.js', function() {
// the callback runs after the script is loaded
newFunction(); // so now it works
...
});
แนวคิดของมัน: argument ตัวที่สอง เป็น function ที่จะทำงานหลังจาก script โหลดเสร็จ
ตัวอย่างการใช้งานจริง:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`Cool, the script ${script.src} is loaded`);
alert( _ ); // _ is a function declared in the loaded script
});
นั่นเรียกว่า “callback-based” เป็นรูปแบบหนึ่งในการเขียน asynchronous
Callback in callback
เราจะโหลด 2 script ตามลำดับได้อย่างไร เช่น โหลดตัวแรกก่อน หลังจากนั้นค่อยโหลดตัวที่สอง
solution โดยปกติเราเรียกใช้ loadScript ภายใน callBack อีกที
loadScript('/my/script.js', function(script) {
alert(`Cool, the ${script.src} is loaded, let's load one more`);
loadScript('/my/script2.js', function(script) {
alert(`Cool, the second script is loaded`);
});
});
และถ้าเราต้องการจะโหลดอีกหลาย ๆ ตัวล่ะ
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// ...continue after all scripts are loaded
});
});
});
Handling errors
ในตัวอย่างข้างบน เราไม่ได้คำนึงถึง Error ที่จะเกิด จะเกิดอะไรขึ้น ถ้าการโหลด script ของเราเกิดไม่สำเร็จขึ้นมา callBack ของเราควรที่จะตอบสนองกับสิ่งนั้น
นี่คือ loadScript เวอร์ชันปรับปรุงที่สามารถตอบโต้กับ Error ได้:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
เมื่อ Script โหลดสำเร็จมันเรียกใช้ callback(null, script) และ callback(error) เมื่อไม่สำเร็จ
loadScript('/my/script.js', function(error, script) {
if (error) {
// handle error
} else {
// script loaded successfully
}
});
ย้ำอีก 1 ครั้ง โค้ดที่เราใช้กับ loadScript ค่อนข้างจะเป็นพื้นฐาน ซึ่งมันเป็นรูปแบบ “error-first callback”
Pyramid of Doom
เมื่อมองแวบแรก ดูเหมือนว่าการเขียนโค้ดแบบนี้ จะสามารถทำงานได้ดี และมันก็ทำงานได้ดีจริง ๆ สำหรับการโหลดเพียง 1 หรือ 2 script
แต่สำหรับการโหลดหลาย ๆ script นั้น เราจะมีโค้ดที่เป็นแบบนี้
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
});
}
});
}
});
ในโค้ดด้านบน:
- เราโหลด
1.js, ไปต่อถ้าไม่มี Error - เราโหลด
2.js, ไปต่อถ้าไม่มี Error - เราโหลด
3.js, ไปต่อถ้าไม่มี Error – ทำอย่างอื่นต่อ(*).
เมื่อเรายิ่งมีโค้ดมากขึ้น โค้ดก็จะลึกขึ้นและจัดการได้ยากมากขึ้น
และการเขียนโค้ดแบบนี้เรียกว่า “callBack hell” หรือ “pyramid of doom”

pyramid ของการเรียกใช้ซ้อนกันไปเรื่อย ๆ จะยาวต่อไปในทางขวา และเมื่อเราเขียนแบบนี้ไปเรื่อย ๆ เราก็จะจัดการโค้ดได้ยากมากขึ้น
ซึ่งในการเขียนโค้ดแบบนี้ ไม่ดีมากนัก
เราสามารถแก้ปัญหานี้ได้โดยการ ทำให้ทุก action เป็น standalone function
loadScript('1.js', step1);
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}
function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}
function step3(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
}
เห็นไหม ว่ามันทำงานเหมือนกัน และโค้ดก็อ่านง่ายมากกว่าด้วย
Promise
ลองนึกภาพว่าคุณเป็นนักร้องชั้นนำ แล้วแฟนๆ ก็ถามหาเพลงใหม่ของคุณทั้งวันทั้งคืน
เพื่อทำให้ทุกคนสบายใจ คุณสัญญาว่าจะส่งเพลงไปให้พวกเขาเมื่อมีการเผยแพร่ คุณให้แฟน ๆ ของคุณ กรอกที่อยู่อีเมลของเขา เพื่อว่าเมื่อเพลงพร้อมเผยแพร่ ทุกคนที่สมัครจะได้รับเพลงทันที และแม้ว่าจะมีอะไรผิดพลาดร้ายแรง เช่น ไฟไหม้ในสตูดิโอ ซึ่งคุณไม่สามารถเผยแพร่เพลงได้ พวกเขาจะยังคงได้รับการแจ้งเตือน
ทุกคนมีความสุข เพราะคุณไม่โดดแฟน ๆ รุม และแฟน ๆ ก็จะไม่พลาดเพลงของคุณ
นี่เป็นการเปรียบเทียบชีวิตจริงกับการเขียนโค้ด
- producing code ทำบางอย่างที่ใช้เวลา ตัวอย่างเช่น การโหลดข้อมูลบน network เปรียบเป็น “นักร้อง”
- consuming code ที่ต้องการผลลัพธ์จาก producing code เมื่อมันพร้อมแล้ว ซึ่ง หลาย ๆ function ต้องการผลลัพธ์นั้น เปรียบเป็น “แฟนคลับ”
- promise เป็น Object พิเศษของ JavaScript ที่จะเชื่อม “producing code” และ “consuming code” เข้าด้วยกัน โดยที่ producing code จะใช้เวลาเท่าที่ต้องการ เพื่อส่งผลลัพธ์ ออกมาและ promise จะทำหน้าที่ทำให้ข้อมูลนั้นสามารถใช้งานได้เมื่อมันพร้อมใช้งาน
้ด ในการเปรียบเทียบนี้ ไม่ได้ถูกต้องมากนัก เพราะว่า promise ใน JavaScript นั้น ค่อนข้างซับซ้อนมากกว่าที่กล่าวมา พวกมันยังมีข้อจำกัด และสิ่งเพิ่มเติมอื่น ๆ อีก
syntax ของ promise:
let promise = new Promise(function(resolve, reject) {
// executor (the producing code, "singer")
});
function ที่ถูกส่งเข้าไปใน new Promise จะถูกเรียกว่า executor. เมื่อ new Promise ถูกสร้างขึ้น executor จะถูกรันโดยอัตโนมัติ. มันเก็บ producing code ไว้ ซึ่งจะส่งผลลัพธ์ออกมาในที่สุด ในการเปรียบเทียบแล้ว โค้ด executor ถูกเปรียบเป็น “นักร้อง”
โดย resolve และ reject คือ callBack ของ JavaScript เอง และโค้ดของเราจะอยู่แค่ภายใน executor
โดยสรุปแล้ว executor จะรันโดยอัตโนมัติ และทำงานของมัน และเมื่อมันทำหน้าที่ของมันเสร็จแล้ว มันจะเรียกใช้ resolve แต่ถ้าไม่สำเร็จ มันจะเรียกใช้ reject
promise จะถูกรีเทิร์นโดย new Promise และมันมี properties ภายในอยู่:
stateเริ่มแรกจะเป็น"pending", หลังจากนั้นเมื่อresolveถูกใช้ จะเปลี่ยนค่าเป็น"fulfilled"ในทางตรงกันข้าม มันจะเปลี่ยนเป็น"rejected"แทนresultเริ่มแรกจะเป็นundefinedและจะเปลี่ยนเป็นvalueเมื่อresolve(value)ถูกเรียกใช้ และในทางตรงข้ามจะเปลี่ยนเป็นerrorแทน

ตัวอย่างของ promise ที่ใช้ producing code ใช้เวลาในการทำงานของมัน (โดยใช้ setTimeout)
let promise = new Promise(function(resolve, reject) {
// the function is executed automatically when the promise is constructed
// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done"), 1000);
});
เราสามารถสองอย่างของโค้ดด้านบน
-
executor ถูกเรียกใช้โดยทันที
-
executor รับมาสอง argument
resolveและreject, function สองตัวนี้ถูกประกาศไว้ก่อนแล้ว โดย JavaScript และเราไม่จำเป็นที่จะสร้างมันหลังจาก 1 วินาทีของการประมวลผล, executor เรียกใช้
resolve("done")ในการให้ผลลัพธ์ออกมา มันเปลี่ยน state ของPromise

ตัวอย่างของ code เมื่อ executor เจอ Error
let promise = new Promise(function(resolve, reject) {
// after 1 second signal that the job is finished with an error
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
ในการเรียกใช้ reject(...) มันจะเปลี่ยน state ของ Object เป็น "rejected" :
โดยสรุปแล้ว executor จะทำงานของมัน และเรียกใช้ resolve และ reject ในการเปลี่ยนแปลง state ให้สอดคล้องกับ promise Object
Consumers: then, catch
Promise object ทำหน้าที่เหมือนกับการเชื่อม executor และ consuming function เข้าด้วยกัน โดยที่จะรับ result หรือ error. Consuming functions สามารถติดตามค่าได้โดยการใช้ .then และ .catch.
then
สิ่งพื้นฐานที่สำคัญที่สุดคือ .then
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);
argument แรกของ .then คือ เมื่อ promise เป็น resolved และรับค่ามา
argument ตัวที่สองของ .then คือเมื่อ promise เป็น rejected และรับค่า Error มา
ตัวอย่างของการทำงานสำเร็จ
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve runs the first function in .then
promise.then(
result => alert(result), // shows "done!" after 1 second
error => alert(error) // doesn't run
);
function แรก ถูก execute
และในกรณีของ rejection
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// reject runs the second function in .then
promise.then(
result => alert(result), // doesn't run
error => alert(error) // shows "Error: Whoops!" after 1 second
);
ถ้าเราต้องการแสดงผล แค่เมื่อเวลาที่มันสำเร็จ เราสามารถใส่ argument แค่ตัวเดียวได้
let promise = new Promise(resolve => {
setTimeout(() => resolve("done!"), 1000);
});
promise.then(alert); // shows "done!" after 1 second
catch
ถ้าเราสนใจแค่ Error เราสามารถใช้ null เป็น argument ตัวแรกได้ .then(null, errorHandlingFunction) หรือเราสามารถใช้ .catch(errorHandlingFunction) ซึ่งจะทำหน้าที่เหมือนกัน
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second
Cleanup: finally
เหมือนกับ finally ใน try {...} catch {...} มันยังมี finally ใน promise อีกด้วย
ในการเรียกใช้ .finally(f) เหมือนกับการเรียกใช้ .then(f, f) ในแง่ที่ว่าไม่ว่าจะเป็น resolve หรือ reject
function f ก็จะทำงานอยู่เสมอ
แนวคิดของ finally คือการจัดการสำหรับดำเนินการ cleanup/finalizing หลังจากการทำงานก่อนหน้านี้เสร็จสิ้น
ตัวอย่างเช่น หยุดหน้าโหลด, ปิด connections ที่ไม่ต้องการ และ อื่น ๆ
ให้ลองคิดเหมือนกับเวลา party เสร็จ ไม่สำคัญว่า party จะดีหรือไม่ดี สุดท้ายก็ต้องทำความสะอาดมันอยู่ดี
Code จะลักษณะประมาณนี้
new Promise((resolve, reject) => {
/* do something that takes time, and then call resolve or maybe reject */
})
// runs when the promise is settled, doesn't matter successfully or not
.finally(() => stop loading indicator)
// so the loading indicator is always stopped before we go on
.then(result => show result, err => show error)
โปรดจำไว้ว่า finally(f) ไม่ได้เหมือนกับ then(f,f) ซักทีเดียว
ข้อแตกต่างที่สำคัญ
-
finallyไม่มีการรับ argument. ในfinallyเราไม่สามารถรู้ได้ว่า promise สำเร็จหรือไม่ -
finallyจะทำการส่งผ่าน result หรือ Error ไปที่ handler ตัวถัดไปตัวอย่างเช่น
finallyส่งผ่านthenไปnew Promise((resolve, reject) => {
setTimeout(() => resolve("value"), 2000);
})
.finally(() => alert("Promise ready")) // triggers first
.then(result => alert(result)); // <-- .then shows "value"อย่างที่เราเห็น
valueถูก return ด้วย promise ตัวแรก ผ่านfinallyไปที่thenเพราะว่า
finallyไม่ได้ถูกสร้างมาเพื่อประมวลผลผลลัพธ์ของ promise อย่างที่พูดไป มันทำหน้าที่ cleanup ไม่สำคัญว่า ผลลัพธ์จะออกมาเป็นอะไรตัวอย่างที่ใช้กับ
catch:new Promise((resolve, reject) => {
throw new Error("error");
})
.finally(() => alert("Promise ready")) // triggers first
.catch(err => alert(err)); // <-- .catch shows the error -
finallyhandler ไม่ควร return ค่าอะไรออกมา, ค่าที่ถูก return ออกมาจะถูกละเว้นข้อยกเว้นเดียวของมันคือ
finallyจะได้โยน Error และ Error นั้นจะไปที่ handler ตัวต่อไป
โดยสรุปแล้ว
finallyhandler ไม่ได้รับค่าที่มาจาก handler ตัวก่อนหน้า (มันไม่มี argument). โดย outcome ที่ได้ออกมา จะถูกส่งผ่านไปที่ handler ตัวถัดไป- ถ้า
finallyreturn ค่าซักอย่างออกมา มันจะถูกละเว้น - ถ้า
finallyโยน Error ออกมา Error จะถูกส่งไปที่ handler ตัวที่ใกล้ที่สุด
Example: loadScript
เรามาดูตัวอย่างในทางปฏิบัติกันดีกว่า ว่าเราสามารถใช้ promises ในการเขียนโค้ดได้อย่างไร
เรามี loadScript function จากบทเรียนที่แล้ว
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
เรามาลองเขียนใหม่โดยใช้ Promise กันเถอะ
เราจะสร้าง loadScript โดยที่เราจะไม่ใช้ callBack ในทางกลับกัน เราจะสร้าง และ rerturn Promise object หลังจากที่มันทำงานเสร็จสิ้นแล้ว และเพิ่ม handlers ให้กับมัน
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
การใช้งาน
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));
Promises chaining
กลับไปปัญหาที่เรากล่าวถึงในบท Introduction: callbacks เรามีลำดับการทำงานแบบ asynchronous ที่ต้องทำงานทีละงาน — ตัวอย่างเช่นการโหลด script เราจะเขียนโค้ดมันอย่างไร
การใช้ Promise มี 2 วิธีที่จะสามารถทำได้
ในบทนี้ เราจะใช้วิธี Promise chaining
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
แนวคิดของมันคือ ผลลัพธ์จะผ่าน Chain ไปเรื่อย ๆ ผ่าน .then handler
หลักการทำงานองมัน:
- promise ตัวแรก ส่งค่าให้ภายใน 1 วินาที
.thenถูกเรียกใช้ และรับผลลัพธ์จากตัวแรก หลังจากนั้นสร้าง promise ใหม่ขึ้นมา และส่งต่อผลลัพธ์ต่อไป.thenถูกเรียกใช้ รับค่าจากตัวก่อนหน้ามา และทำการ processes (เพิ่มค่าคูณ 2) และส่งไปที่ตัวถัดไป- และไปเรื่อย ๆ
ทุกสิ่งทำงานได้ เพราะว่าทุกครั้งที่เรียกใช้ .then มันจะ return promise ใหม่มาและเราก็เรียกใช้ .then ไปเรื่อย ๆ เมื่อ handler return ค่าออกมา มันจะมาเป็นผลลัพธ์ของ promise ดังนั้น .then ตัวต่อไป จึงเรียกใช้งานได้
ข้อผิดพลาดของมือใหม่: ตามเทคนิคเราสามารถใช้ .then กับ promise ตัวเดียวได้ ที่ไม่ใช่ chaining
ตัวอย่างเช่น
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
ที่เราทำคือการที่ เพิ่ม handler หลาย ๆ ตัวไปที่ promise แค่ตัวเดียว. พวกมันไม่ได้ส่งผลลัพธ์ให้กัน แต่มันจะประมวลผลแยกกัน
.then ทุกตัวจะได้ผลลัพธ์เดียวกัน ดังนั้นโค้ดด้านบนทั้งหมด จะโชว์ผลลัพธ์ alert เหมือนกันหมดคือ 1
Returning promises
handler ที่ใช้ใน .then(handler) มันอาจจะสร้างและ return promise ได้
ในกรณีที่ handlers ต้องรอการที่จะได้ผลลัพธ์ออกมา
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
.then ตัวแรกโชว์ค่า 1 ออกมาและ return promise ออกไปในบรรทัด (*) หลังจาก 1 วินาที มันส่งผลลัพธ์ไปที่ handler ตัวถัดไป และ handler ตัวถัดไป ก็โชว์ค่า 2 และเหมือนกัน handler ตัวสุดท้าย ก็โชว์ค่า 4
ดังนั้น ผลลัพธ์ที่เราได้ก็เหมือนกับ ตัวอย่างก่อนหน้า 1 → 2 → 4 แต่เราได้เพิ่ม delay เข้าไป 1 วินาที
Example: loadScript
เรามาลองใช้ feature ของ promise ในการโหลด script กัน โดยโหลดไปเรื่อย ๆ ตามลำดับ
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// use functions declared in scripts
// to show that they indeed loaded
one();
two();
three();
});
เราสามารถเขียนให้สั้นลงได้ โดยการใช้ Arrow function
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// scripts are loaded, we can use functions declared there
one();
two();
three();
});
loadScript อีกตัว คืนค่า promise ออกไป และ .then ตัวต่อไปจะถูกรันเมื่อตัวก่อนหน้าโหลดเสร็จแล้ว
เราสามารถเพิ่ม asynchronous actions เข้าไปอีกได้ เพราะว่าโค้ดที่เราเห็นยังสวยงามอยู่ และโค้ดไม่ได้โตไปทางขวา และไม่มีสัญญาณของการเกิด “pyramid of doom”
Bigger example: fetch
ในการเขียน front-end, promises จะถูกใช้เพื่อ network requests. เรามาดูตัวอย่างเพิ่มเติมกัน
เราจะใช้ method fetch ในการ โหลดข้อมูล จาก server
let promise = fetch(url);
นี่ทำให้ network สร้าง request ไปที่ url และ return promise ออกมา. โดย promise resolve มากับ response Object เมื่อ server responds header มา แต่ว่าก่อนที่ response ทั้งหมดจะถูกโหลด
ในการที่จะอ่านข้อมูล เราจะใช้ response.text() มันจะ return promise resolve เมื่อข้อมูลทั้งหมดถูกโหลดเสร็จแล้ว
โดยโค้ดด้านล้าง จะสร้าง request ไปที่ user.json และโหลดข้อความมาจาก server
fetch('/article/promise-chaining/user.json')
// .then below runs when the remote server responds
.then(function(response) {
// response.text() returns a new promise that resolves with the full response text
// when it loads
return response.text();
})
.then(function(text) {
// ...and here's the content of the remote file
alert(text); // {"name": "iliakan", "isAdmin": true}
});
response object จะถูก return ค่ามาจาก fetch ที่จะมี method response.json() ที่อ่านข้อมูลของ json
ในกรณีของเรานั้นสะดวกกว่า ดังนั้นเรามาเปลี่ยนไปใช้กันดีกว่า
เราจะใช้ Arrow function กัน
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // iliakan, got user name
เรามาทำบางอย่างกับข้อมูล user กัน
ตัวอย่างเช่น เราจะสร้าง request ไปที่ Github, โหลด profile user ออกมา และแสดง avatar
// Make a request for user.json
fetch('/article/promise-chaining/user.json')
// Load it as json
.then(response => response.json())
// Make a request to GitHub
.then(user => fetch(`https://api.github.com/users/${user.name}`))
// Load the response as json
.then(response => response.json())
// Show the avatar image (githubUser.avatar_url) for 3 seconds (maybe animate it)
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => img.remove(), 3000); // (*)
});
เราสามารถเห็นรายละเอียดใน comment ได้ แต่มันอาจมีปัญหาเกิดขึ้น ข้อผิดพลาดนี้สามาถเห็นได้เวลาใช้ promise
ดูที่บรรทัด (*) เราจะสามาถทำอย่างอื่นได้หลังจาก avatar โชว์เสร็จแล้วได้อย่างไร ตัวอย่างเช่น เราต้องการที่จะโชว์ Form ในการแก้ไข user หรืออย่างอื่น ณ ตอนนี้ เราไม่สามารถทำได้
ในการทำสิ่งอื่น ๆ ต่อได้ เราต้อง return ค่า promise ที่ resolve เมื่อ avatar โขว์เสร็จแล้วออกไป
fetch('/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(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
// triggers after 3 seconds
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
.then ในบรรทัด (*) **return **new Promise ** จะทำงานได้ก้ต่อเมื่อเรียกใช้ resolve(githubUser) ใน setTimeout() /(**) และ .then ตัวต่อไปจะรอมัน
good practice. asynchronous action ควรจะ return promise ออกมา เพื่อที่จะทำงานอย่างอื่นต่อได้ ถึงแม้เราจะไม่ได้ใช้ผลลัพธ์ของมันต่อ
เราแยกโค้ดเป็น function ที่สามารถใช้ซ้ำได้
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return loadJson(`https://api.github.com/users/${name}`);
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// Use them:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
Summary
ถ้า .then handler return ค่าที่เป็น promise ส่วนที่เหลือของ chain จะรอจนกว่าจะได้ค่า เมื่อมันได้แล้ว ผลลัพธ์ของมันจะถูกส่งต่อไป

Error handling with promises
Promise chains ทำได้ดีมากในเรื่องของการรับมือกับ Error เมื่อ promise เกิดการ reject ขึ้น มันจะกระโดดไปที่ rejection handler ที่ใกล้ที่สุด ซึ่งเป็นเรื่องที่สะดวกสบายมาก
ตัวอย่างเช่น โค้ดบ้านล่าง fetch ไปที่ URL ที่ไม่มีอยู่ และ .catch ทำการรับมือกับ Error
fetch('https://no-such-server.blabla') // rejects
.then(response => response.json())
.catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)
อย่างที่เห็น .catch ไม่จำเป็นต้องทำหน้าที่โดยทันที มันอาจจะเกิดขึ้นหลังจาก 1 หรือ หลาย ๆ .then.
หรือบางที ทุกอย่างปกติดี แต่ response ของ JSON นั้น ไม่ถูกต้อง ทางที่ง่ายที่สุดในการจับ Error ก็สามารถใช้ catch ได้เหมือนกัน ณ ที่ล่างสุดของ chain
fetch('/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((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
โดยปกติแล้ว .catch จะไม่ทำงานเลย แต่หาก Promise อันใดอันนึง rejecet (หรือ ปัญหา network, invalid JSON หรือ อื่น ๆ) ก็จะไปเข้า .catch เหมือนกัน
Implicit try…catch
โค้ดของ promise executor และ promise handlers มี “try..catch ล่องหน” รอบ ๆ มัน, ถ้ามีข้อผิดพลาดเกิดขึ้น มันจะโดนจับ และจะถูกปฏิบัติเป็นเหมือน rejection
ตัวอย่างของ code:
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!
ซึ่งจะเหมือนกับ
new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!
“try..catch ล่องหน” รอบ ๆ executer จะจับ Error โดยอัตโนมัติ และเปลี่ยนมันเป็น rejection promise
นี่ไม่ได้เกิดแค่ใน executor function แต่ว่าภายใน handler ก็เช่นกัน ถ้าเรา throw ใน .then นั่นก็จะเป็น rejected promise เหมือนกัน ดังนั้น มันจะกระโดดไปที่ Error haandler ที่ใกล้ที่สุด
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
throw new Error("Whoops!"); // rejects the promise
}).catch(alert); // Error: Whoops!
มันเกิดกับ Error ทุกรูปแบบไม่ใช่แค่กับ throw statement ตัวอย่างเช่น การเขียนโค้ดผิด
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
blabla(); // no such function
}).catch(alert); // ReferenceError: blabla is not defined
Rethrowing
อย่างที่เรารู้ .catch ที่จุดท้ายสุดของโค้ดจะเป็นเหมือนกับ try..catch. เราอาจมีหลาย .then เท่าที่เราต้องการและใช้แค่ .catch ตัวเดียว ในการจัดการกับ Error ท้้งหมด
ใน try..catch ธรรมดา เราสามารถวิเคราะห์ error ได้ และสามารถ rethrow มันได้ ถ้ามันไม่สามารถ handled ได้ และเราก็สามารถทำได้เหมือนกันกับ Promise
ถ้าเรา throw ภายใน .catch ดังนั้น การควบคุม Error จะไปที่ Error handler ที่ใกล้ที่สุด และถ้าการ handle Error เป็นไปได้อย่างปกติ มันจะไปต่อกับ .then handler ที่ใกล้ที่สุด
ตัวอย่างข้างล่าง .catch สามารถรับมือกับ Error ได้อย่างปกติ
// the execution: catch -> then
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(function(error) {
alert("The error is handled, continue normally");
}).then(() => alert("Next successful handler runs"));
.catch ทำงานได้ปกติดี ดังนั้น .then ตัวต่อไป ถูกเรียกใช้งาน
ตัวอย่างด้านล่าง เราจะมาดูสถานการณ์ที่ .catch (*) ไม่สามารถจัดการกับ Error ได้ ดังนั้นมันจึงทำการ rethrow
// the execution: catch -> catch
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// handle it
} else {
alert("Can't handle such error");
throw error; // throwing this or another error jumps to the next catch
}
}).then(function() {
/* doesn't run here */
}).catch(error => { // (**)
alert(`The unknown error has occurred: ${error}`);
// don't return anything => execution goes the normal way
});
Unhandled rejections
จะเกิดอะไรขึ้นหาก Error ไม่ถูกรับมือ ตัวอย่างเช่น เราลืมใส่ .catch ไปที่สุดท้ายของ chain แบบนี้
new Promise(function() {
noSuchFunction(); // Error here (no such function)
})
.then(() => {
// successful promise handlers, one or more
}); // without .catch at the end!
ในกรณีนี้ promise จะเป็น reject และ execution จะกระโดดไปที่ rejection handler ที่ใกล้ที่สุด แต่มันไม่มี ดังนั้นมันจะเกิดการ “stuck”.
ในทางปฏิบัติ มันเหมือนกับ Error ที่ไม่สามารถรับมือได้ในการเขียนโค้ดแบบธรรมดา นั่นแปลว่าบางอย่างผิดพลาดอย่างร้ายแรง
จะเกิดอะไรขึ้นเมื่อ error ธรรมดาไม่ได้ถูกจับโดย try..catch? script จะพังและมีข้อความขึ้นใน console ซึ่งจะเหมือนกับ promise rejections ที่ไม่สามารถรับมือได้
JavaScript ติดตาม rejections และสร้าง global error ขึ้นมา ในกรณีนี้ คุณสามารถเห็นมันได้ใน console ถ้าคุณรันโค้ดด้านบน
ใน Browser เราสามารถจับ Error ได้โดยใช้ event unhandledrejection:
window.addEventListener('unhandledrejection', function(event) {
// the event object has two special properties:
alert(event.promise); // [object Promise] - the promise that generated the error
alert(event.reason); // Error: Whoops! - the unhandled error object
});
new Promise(function() {
throw new Error("Whoops!");
}); // no catch to handle the error
ถ้าเกิด Error ขึ้นและไม่มี .catch ตัว unhandledrejection handler จะรับ event Object กับข้อมูลของ Error มา เผื่อเราสามารถทำอะไรกับมันได้
ส่วนใหญ่แล้วเมื่อเราเจอปัญหาที่เราไม่สามารถควบคุมได้ ทางที่ดีที่สุดของเราคือแจ้งไปที่ user เกี่ยวกับปัญหาและแจ้งกลับไปที่ server
Summary
.catchใช้จับ Error ใน promises ทุกรูปแบบ เหมือนกับการเรียกใช้reject()และโยน Error ไปที่ handler.thenก็สามารถจับ Error ได้เหมือนกัน ถ้าเราให้ argument ตัวที่สองมา- เราควรวาง
.catchตรงจุดที่เราต้องการจัดการ Error และรู้วิธีการจัดการ Error เหล่านั้น. Error hanlder ควรวิเคราะห์ Error และ rethrow เมื่อเจอ Error ที่เราไม่รู้จัก - เป็นสิ่งที่รับได้ถ้าเราไม่มี
.catchเลย ถ้าเราไม่มีทางที่จะรับมือกับมันเลย - ในทุกกรณี เราควรมี
unhandledrejectionevent handler ในการติดตาม errors และแจ้งไปที่ user เพื่อที่ app ของเราจะได้ไม่ตายไปเฉย ๆ
Promise API
มี 6 API method ของ promise ที่เราจะมาเรียนกันอย่างรวดเร็ว
Promise.all
สมมติว่า เราต้องการ execute promises หลาย ๆ ตัว พร้อม ๆ กัน และรอจนกว่าทุกตัวจะพร้อมใช้งาน
ตัวอย่างเช่น เราต้องการโหลด URL หลาย ๆ ตัวพร้อมกัน และ process content พร้อมกันเมื่อทุกตัวพร้อม
และนั่นคือจุดประสงค์ของ Promise.all
syntax:
let promise = Promise.all(iterable);
Promise.all จะรับ iterable (โดยปกติจะเป็น array ของ Promise) และ return promise ใหม่ออกไป
promise ตัวใหม่จะเป็น resolve เมื่อ promise ทุกตัวเป็น resolve จะผลลัพธ์ของมันจะออกมาเป็น Array
ตัวอย่างเช่น Promise.all จะเสร็จทั้งหมดเมื่อครบ 3 วินาที และผลลัพธ์ของมันจะออกมาเป็น Array [1, 2, 3]:
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member
โปรดจำไว้ว่า การเรียงลำดับของผลลัพธ์จะเป็นตามเหมือนกับที่เราเรียงใน array ถึงแม้ตัวแรกจะใช้เวลานานที่สุด แต่ผลลัพธ์ของมันจะออกมาเป็นตัวแรกของ Array
เคล็ดลับของการ map Array ของ Job data ไปเป็น Array ของ Promises หลังจากนั้น จับมันใส่เข้าไปใน Promise.all.
ตัวอย่างเช่น เรามี Array ของ URL และเราสามารถ fetch พวกมันได้ทั้งหมด แบบนี้
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig'
];
// map every url to the promise of the fetch
let requests = urls.map(url => fetch(url));
// Promise.all waits until all jobs are resolved
Promise.all(requests)
.then(responses => responses.forEach(
response => alert(`${response.url}: ${response.status}`)
));
ในตัวอย่างที่ใหญ่ขึ้น เราจะ fetch ข้อมูล user ของ Github user โดยใช้ชื่อของ user
**let names = ['iliakan', 'remy', 'jeresig'];
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
Promise.all(requests)
.then(responses => {
// all responses are resolved successfully
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // shows 200 for every url
}
return responses;
})
// map array of responses into an array of response.json() to read their content
.then(responses => Promise.all(responses.map(r => r.json())))
// all JSON answers are parsed: "users" is the array of them
.then(users => users.forEach(user => alert(user.name)));**
ถ้า promises บางตัว rejected. promise ที่ return โดย Promise.all จะ reject มันโดยทันที
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!
promise ตัวที่สองเกิด reject ขึ้น นั่นทำให้เกิดการ rejected ของ Promise.all ดังนั้น .catch ที่จับ rejection จะมาเป็นผลลัพธ์ของทั้ง Promise.all.
Promise.allSettled
Promise.all จะเป็น reject เมื่อมีตัวใดตัวนึงเป็น reject นั่นเป็นสิ่งที่ดีสำหรับกรณีที่ว่า “all or nothing” เมื่อเราต้องการการประมวลผลที่สำเร็จทั้งหมด
Promise.all([
fetch('/template.html'),
fetch('/style.css'),
fetch('/data.json')
]).then(render); // render method needs results of all fetches
Promise.allSettled จะรอสำหรับ promise ทุกตัวเสร็จสิ้น โดยไม่นึกถึงผลลัพธ์ และผลลัพธ์ของ array เป็น:
{status:"fulfilled", value:result}สำหรับ responses ที่สำเร็จ,{status:"rejected", reason:error}สำหรับ Error.
ตัวอย่างเช่น เราต้องการข้อมูลของ user หลาย ๆ คน ถึงแม้จะมี request หนึ่งผิดพลาด เราก็ยังต้องการข้อมูลของคนอื่นอยู่
เรามาลองใช้งานมันกันเถอะ
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
.then(results => { // (*)
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});
results ในบรรทัด (*) จะเป็น
[
{status: 'fulfilled', value: ...response...},
{status: 'fulfilled', value: ...response...},
{status: 'rejected', reason: ...error object...}
]
status ที่เราได้ของ promise แต่ละตัวที่เราจะได้เป็น value/error.
Promise.race
เหมือนกับ Promise.all, แต่จะเอาแค่ตัวที่เร็วที่สุดและรับค่าผลลัพธ์หรือ Error มา
syntax:
let promise = Promise.race(iterable);
ตัวอย่างการใช้:
Promise.race([เ
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
Promise ตัวแรกเป็นตัวที่เร็วสุด ดังนั้นมันจะมาเป็นผลลัพธ์ของโค้ด
Promise.any
เหมือนกับ Promise.race แต่จะรอแค่ตัวแรกที่เป็น fulfilled promise และดึงค่าผลลัพธ์มา ถ้า promises ทั้งหมดเป็น reject ดังนั้นมันจะ return promise เป็น rejected กับ AggregateError — object พิเศษที่เก็บ errors ของ promise ใน errors property
let promise = Promise.any(iterable);
ตัวอย่างการใช้งาน
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
promise ตัวแรกเป็นตัวที่เร็วสุดแต่ว่า มันดันเป็น rejected ดังนั้น promise ตัวที่สองจึงมาเป็นผลลัพธ์
ตัวอย่างเมื่อ promise เป็น rejected ทั้งหมด
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
console.log(error.constructor.name); // AggregateError
console.log(error.errors[0]); // Error: Ouch!
console.log(error.errors[1]); // Error: Error!
});
Promise.resolve/reject
Methods Promise.resolve และ Promise.reject อาจไม่ค่อยได้ใช้งานในการเขียนโค้ดสมัยนี้ เพราะ async/await ทำให้มันล้าสมัยไปบ้าง
ซึ่งเราจะมาบอกวิธีใช้ เผื่อมีบางกรณีที่เราไม่สามารถใช้ async/await ได้
Promise.resolve
Promise.resolve(value) สร้าง resolved promise กับ ผลลัพธ์ value.
เหมือนกับ
let promise = new Promise(resolve => resolve(value));
method นี้ใช้เมื่อ function คาดว่าจะ return promise ออกมา
ตัวอย่างเช่น loadCached ข้างล่าง fetches URL และจำ content ของมัน(caches) สำหรับการเรียกใช้ซ้ำในอนาคต กับ URL เดิม โดยมันจะเรียก content ก่อนหน้าจาก cache แต่เราจะใช้ Promise.resolve เพื่อทำให้มันเป็น promise และจะ return ค่าของมันออกมาเป็น promise เสมอ
let cache = new Map();
function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}
return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}
เราสามารถเขียน loadCached(url).then(…), ได้เพราะว่า function จะรับประกันที่จะ return promise ออกมา
Promise.reject
Promise.reject(error) จะสร้าง rejected promise กับ Error มา
เหมือนกับ
let promise = new Promise((resolve, reject) => reject(error));
Summary
มี 6 static methods ของ promise
Promise.all(promises)– รอจนกว่า promise ทุกตัวจะเป็น resolve. ถ้ามีตัวใดตัวนึงเป็น rejected, มันจะมาเป็น Error ของPromise.all, และผลลัพธ์ของอันอื่น ๆ จะถูกละเว้น.Promise.allSettled(promises)(method นี้พึ่งถูกเพิ่มเข้ามา) – รอให้ promise ทุกตัวทำงานเสร็จและ return ค่าของ array ที่เป็น Object โดยจะเก็บค่าเป็น:status:"fulfilled"หรือ"rejected"value(ถ้า fulfilled) หรือreason(ถ้า rejected).
Promise.race(promises)– รอจนกว่าตัวไหนจะเสร็จเป็นตัวแรก, โดยไม่สนว่าผลลัพธ์จะดีหรือเป็น ErrorPromise.any(promises)(method นี้พึ่งถูกเพิ่มเข้ามา) – รอจนกว่า promise ตัวไหนจะเป็น fulfill และเสร็จเป็นตัวแรก, และผลลัพธ์ของมันจะออกมาเป็น outcome. ถ้า promise ทุกตัวเป็น rejected,AggregateErrorจะมาเป็น Error ของPromise.anyเพื่อบอกเหตุผลของ ErrorPromise.resolve(value)– สร้าง resolved promise ด้วยค่าที่มอบให้.Promise.reject(error)– สร้าง rejected promise ด้วย Error ที่มอบให้.
Promisification
Promisification เป็นคำสำหรับการเปลี่ยนแปลง มันเปลี่ยนจาก function ที่รับ callBack ไปเป็น function ที่ return promise ออกไป
เพื่อให้เข้าใจมากขึ้น เรามาดูตัวอย่างกัน
ตัวอย่างเช่น เรามี loadScript(src, callback) จากบทแรก
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
// usage:
// loadScript('path/script.js', (err, script) => {...})
function จะโหลด script ตามที่เราให้ src ไป และเรียกใช้ callback(err) ในกรณีที่เกิด Error หรือใช้ callback(null, script) เมื่อโหลดสำเร็จ
เรามี promisify มันกัน
เราจะสร้าง function ใหม่ loadScriptPromise(src) ที่จะทำงานเหมือนกับ loadScript แต่จะ return เป็น promise ออกมา แทนการใช้ callBack
พูดง่าย ๆ เราจะส่ง src ไปให้มัน จะรับ promise กลับมา
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
};
// usage:
// loadScriptPromise('path/script.js').then(...)
เห็นได้ว่า function ใหม่นี้คลุมตัว loadScript ไว้ข้างใน โดยมันเรียกใช้ callBack ของมัน และเปลี่ยนเป็น resolve/reject
ตอนนี้ loadScriptPromise สามารถใช้งานได้ตามปกติแล้ว ถ้าเราชอบใช้ promise มากกว่าวิธี callBack เราก็สามารถใช้มันได้
ในการใช้งานจริง เราอาจต้องการ function คล้าย ๆ นี้ หลาย ๆ ตัว ดังนั้นเราจะมาสร้างตัวที่ช่วยสร้าง function แบบนี้กัน
เราจะเรียกมันว่า promisify(f) มันจะรับ to-promisify function f และ return fucntion ที่ถูก wrap ออกมา
function promisify(f) {
return function (...args) { // return a wrapper-function (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // our custom callback for f (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}
args.push(callback); // append our custom callback to the end of f arguments
f.call(this, ...args); // call the original function
});
};
}
// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
โค้ดอาจมีความซับซ้อน แต่เอาจริง ๆ มันเหมือนกับโค้ดที่เราเขียนไปก่อนหน้าเลย
การเรียกใช้ promisify(f) จะ return wrapper รอบ ๆ f (*). โดย wrapper นั้น return promise และ เรียกใช้ function นั้น และติดตามผลลัพธ์ใน callback ที่เราสร้างขึ้น callback
สมมติว่า promisify สามารถใส่ได้แค่ 2 argument (err, result). ดังนั้น callBack ของเรา อยู่ใน format ที่ถูกต้องและ promisify สามารถทำงานได้อย่างปกติ ในกรณีนี้
แต่ถ้าเราต้องการมากกว่านั้นล่ะ callback(err, res1, res2, ...)?
เรามาทำ promisify ที่เหนือขั้นไปมากกว่านี้กัน
promisify(f)ควรจะเป็นเหมือนโค้ดที่เราเขียนไปด้านบน- เมื่อเราใช้
promisify(f, true)มันควรจะ return promise resolves ที่ผลลัพธ์เป็น Array ของ callback ดังนั้นโค้ดเราจะสามารถรับ argument หลายตัวได้
// promisify(f, true) to get array of results
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // our custom callback for f
if (err) {
reject(err);
} else {
// resolve with all callback results if manyArgs is specified
resolve(manyArgs ? results : results[0]);
}
}
args.push(callback);
f.call(this, ...args);
});
};
}
// usage:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);
อย่าที่คุณเห็น โค้ดที่เราปรับปรุง เหมือนกับโค้ดเวอร์ชั่นเก่า แต่ resolve return ค่าขึ้นอยู่กับ manyArgs
JavaScript ยังมี modules ที่สามารถช่วยในเรื่องนี้ได้ ซึ่งเป็น buitl-in function ที่อยู่ใน Node.js util.promisify
Microtasks
Promise handlers .then/.catch/.finally จะทำงานแบบ asynchronous เสมอ
แม้ว่า promise จะได้ resolve โดยทันที แต่โค้ดที่อยู่ด้านล่าง .then, .catch, และ .finally จะทำงานก่อน handler เหล่านั้น
let promise = Promise.resolve();
promise.then(() => alert("promise done!"));
alert("code finished"); // this alert shows first
ถ้าคุณลองรันมันดู จะสังเกตได้ว่า code finished จะขึ้นมาก่อน promise done!
นั่นดูแปลกมาก เพราะว่า promise เหมือนจะทำงานเสร็จตั้งนานแล้ว แต่ทำไม .then ถึงพึ่งมาทำงานล่ะ
Microtasks queue
งาน Asynchronous ต้องการการจัดการอย่างเหมาะสม ดังนั้นมาตรฐาน ECMA จึงกำหนด internal queue ที่เรียกว่า PromiseJobs หรือมักจะเรียกว่า "microtask queue"
ตามที่ระบุใน specification:
- คิวเป็นแบบ first-in-first-out (FIFO): งานที่เข้าคิวก่อนจะถูกดำเนินการก่อน
- การดำเนินงานจะเริ่มต้นเฉพาะเมื่อไม่มีโค้ดอื่นกำลังทำงานอยู่
หรือกล่าวง่าย ๆ เมื่อ Promise พร้อม, handler .then, .catch, หรือ .finally จะถูกใส่เข้าไปในคิว และยังไม่ถูก executed ทันที, และเมื่อ JavaScript ทำงานเสร็จจากโค้ดปัจจุบัน จะดึงงานจากคิวและดำเนินการ
นี่คือสาเหตุที่ทำให้ "code finished" ในตัวอย่างข้างต้นแสดงก่อน

ถ้ามี .then/catch/finally หลาย ๆ ตัว ดังนั้นทุกตัวจะ executed แบบ asynchronously มันจะไปเข้าคิวก่อน ดังนั้น มันจะ executed โค้ดปัจจุบันก่อนและหลังจากนั้น handlers ที่อยู่ในคิว ค่อยถูก executed
ถ้าเราต้องการทำให้ ****code finished โชว์หลัง promise done ****เราจะทำได้อย่างไร
ง่ายมาก เราแค่จับมันใส่ .then
Promise.resolve()
.then(() => alert("promise done!"))
.then(() => alert("code finished"));
Unhandled rejection
ยังจำ unhandledrejection event จาก Error handling with promises ได้หรือไม่
ตอนนี้เราจะมาดูว่า JavaScript รู้ได้อย่างไรว่ามี unhandled rejection
การเกิด "unhandled rejection" เกิดขึ้นเมื่อมีข้อผิดพลาดใน promise ที่ไม่ได้รับการจัดการเมื่อจบการประมวลผลของ microtask queue
ตามปกติ หากเราคาดว่าจะมี Error เกิดขึ้น เราจะเพิ่ม .catch ลงใน chain promise เพื่อจัดการกับ Error :
let promise = Promise.reject(new Error("Promise Failed!"));
promise.catch(err => alert('caught'));
// doesn't run: error handled
window.addEventListener('unhandledrejection', event => alert(event.reason));
แต่ถ้าเราลืมใส่ .catch ดังนั้นหลังจาก microtask queue ว่างเปล่าแล้ว JavvaScript engine จะ triggger event
let promise = Promise.reject(new Error("Promise Failed!"));
// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));
จะเกิดอะไรขึ้นเมื่อ Error เกิดช้าล่ะ
let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')), 1000);
// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));
เมื่อรันโค้ดนี้ จะเห็นข้อความ "Promise Failed!" แสดงขึ้นก่อน และตามด้วย "caught"
ถ้าเราไม่รู้เรื่องคิวไมโครทาสก์ เราอาจสงสัยว่า: “ทำไม unhandledrejection จึงทำงาน ทั้งๆ ที่เรา catch Error แล้ว?”
แต่ตอนนี้เราทราบแล้วว่า unhandledrejection จะถูกเรียกเมื่อ microtask queue ทำงานเสร็จสมบูรณ์: Engine จะตรวจสอบ promise และถ้าหากมี promise ใดอยู่ในสถานะ "rejected" event นี้จะถูกเรียก
ในตัวอย่างด้านบน .catch ที่ถูกเพิ่มด้วย setTimeout ก็ถูกเรียกเช่นกัน แต่มันทำงานภายหลังจากที่ unhandledrejection ถูกเรียกแล้ว ดังนั้นมันจึงไม่เปลี่ยนแปลงผลลัพธ์อะไร
Summary
promise handling จะทำงานแบบ asynchronous เสมอ เนื่องจากการทำงานทั้งหมดของ promise จะผ่านคิวภายในที่เรียกว่า “promise jobs” หรือ “microtask queue”
ดังนั้น .then, .catch, และ .finally จะถูกเรียกหลังจากโค้ดปัจจุบันทำงานเสร็จ
หากเราต้องการให้มีโค้ดบางส่วนทำงานหลังจาก .then, .catch, หรือ .finally เราสามารถเพิ่มโค้ดนั้นเข้าใน .then ที่ต่อกันเป็น chain
ใน JavaScript Engine ส่วนใหญ่ ทั้งใน Browser และ Node.js แนวคิดเรื่อง microtasks จะเกี่ยวข้องโดยตรงกับ event loop และ macrotasks (งานขนาดใหญ่) ซึ่งหัวข้อนี้จะไม่ได้เกี่ยวข้องโดยตรงกับ Promise ดังนั้นจะถูกอธิบายในบทความเกี่ยวกับ Event loop: microtasks and macrotasks.
Async/await
มี syntax พิเศษ ที่ใช้ทำงานร่วมกับ promise ในทางที่สะดวกมากกว่า เรียกว่า “async/await”. มันเป็นเรื่อง่ายมากในการเข้าใจและใช้งาน
Async functions
เรามาเริ่มกับ async ซึ่งมันจะวางไว้ที่ก่อนหน้า function แบบนี้:
async function f() {
return 1;
}
async ก่อน function หมายความว่า function นั้นจะ return เป็น promise ออกมาเสมอ และค่าอื่น ๆ จะถูก wrap อยู่ใน resolved promise
ตัวอย่างเช่น function นี้ return resolved promise กับค่า 1 ออกมา
async function f() {
return 1;
}
f().then(alert); // 1
ซึ่งจะเหมือนกับ
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
ดังนั้น async จะรับประกันว่า มันจะ return promise ออกมา แต่ไม่ใช่แค่นั้น เรายังมีอีก keyword คือ await นั่นสามารถทำงานได้ แค่ตอนที่อยู่ใน async
Await
syntax:
// works only inside async functions
let value = await promise;
await จะรอจนกว่า promise จะทำงานเสร็จและ return ค่าออกมา
นี่คือตัวอย่างของ promise ที่จะ resolve ภายใน 1 วินาที
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
f();
function execution “หยุด” ที่บรรทัด (*) และทำงานต่อ หลังจากมันทำงานเสร็จสิ้นแล้ว และ result จะมาเป็น ผลลัพธ์ ดังนั้น โค้ดด้านบนจะโชว์ done! ภายใน 1 วินาที
ขอเน้นย้ำ: await หยุดการทำงานของ function จนกว่า promise จะทำงานเสร็จสิ้น และ ทำงานต่อเมื่อ promise ทำงานเสร็จแล้ว นั่นไม่ได้ทำให้ CPU ทำงานหนักขึ้น เพราะว่า JavaScript ยังสามารถทำงานอื่นต่อได้ในเวลานั้น
เราลองมาเขียน showAvatar() ใหม่กัน
- เราต้องแทนที่
.thenเป็นawaitแทน - แล้วเราก็ควรทำให้ function
asyncทำงานได้กับมันด้วย
async function showAvatar() {
// read our JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// read github user
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// show the avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// wait 3 seconds
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
มันดูง่ายมากเลยใช่มั้ย ดีกว่าแต่ก่อนมาก
Error handling
ถ้า promise resolve ปกติ ดังนั้น await promise ก็ทำงานตามปกติ แต่ถ้าในกรณีที่เกิด rejection ล่ะ
มันจะ throw Error แค่มันมี throw ในบบรทัด
โค้ดนี้
async function f() {
await Promise.reject(new Error("Whoops!"));
}
เหมือนกับ
async function f() {
throw new Error("Whoops!");
}
ในสถานการณ์จริง promise อาจใช้เวลาก่อนจะ rejects ในกรณีนั้น await จะ delay ก่อนที่จะ throw Error
เราสามารถจับ Error ได้โดยใช้ try..catch
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
ในกรณีของ Error นั้น มันจะกระโดดไปที่ catch โดยเราอาจมี Error ได้ในหลายจุด
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}
f();
ถ้าเราไม่มี try..catch เราก็สามารถใช้ .catch สำหรับรับมือกับ Error ได้
async function f() {
let response = await fetch('http://no-such-url');
}
// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)
ถ้าเราลืมใส่ .catch เราสามารถใช้ event unhandledrejection ในการรับมือกับ Error ได้
Summary
การใช้ async วางไปที่หน้าของ function จะทำให้ function เกิด effects สองอย่าง
- จะทำให้ function return promise ออกมาตลอด
- อนุญาตให้ใช้
awaitได้
การใช้ await ก่อน promise ทำให้ JavaScript รอจนกว่า promise จะทำงานเสร็จ หลังจากนั้น
- ถ้ามี Error, exception จะถูกสร้างขึ้น เหมือนกับการใช้
throw error - ถ้าไม่มี Error มันจะสร้างผลลัพธ์ให้เรา
ทั้งหมดนี้ทำให้เกิด Framework ดี ๆ ขึ้น ที่สามารถอ่านได้ง่ายและเขียนได้ง่าย
async/await เราอาจได้ใช้ promise.then/catch น้อยลง แต่เราก็ไม่ควรลืมพื้นฐานของ promise เพราะบางครั้งเราก็ต้องใช้ method ของมันเช่น Promise.all เมื่อเราต้องการจะรอหลาย ๆ การทำงาน