JavaScript Async/Await ใช้ยังไงให้ถูก – 30 เมษายน 2569

JavaScript Async/Await ใช้ยังไงให้ถูก: คู่มือฉบับสมบูรณ์เพื่อการเขียน Code ที่มีประสิทธิภาพ

ในโลกของการพัฒนาเว็บแอปพลิเคชันด้วย JavaScript ปัญหาที่นักพัฒนาต้องเผชิญอยู่บ่อยครั้งคือการจัดการกับการทำงานแบบ Asynchronous หรือการทำงานที่ไม่รอผลลัพธ์ให้เสร็จสิ้นก่อนจะไปทำงานบรรทัดถัดไป เช่น การดึงข้อมูลจาก API การอ่านไฟล์ หรือการเชื่อมต่อฐานข้อมูล ในอดีตเราอาจคุ้นเคยกับการใช้ Callback Functions จนเกิดปัญหา “Callback Hell” หรือต่อมาได้เปลี่ยนมาใช้ Promises ที่ช่วยให้โค้ดอ่านง่ายขึ้น แต่ก็ยังมีความซับซ้อนในการทำ Chaining อยู่ดี

จนกระทั่งการมาถึงของ Async/Await ในมาตรฐาน ES2017 ซึ่งถูกออกแบบมาเพื่อให้เราสามารถเขียนโค้ด Asynchronous ให้มีหน้าตาและลำดับการทำงานเหมือนกับ Synchronous Code (โค้ดที่รันทีละบรรทัดจากบนลงล่าง) สิ่งนี้ไม่ได้เป็นเพียงแค่ Syntactic Sugar หรือการเปลี่ยนรูปแบบการเขียนเท่านั้น แต่มันช่วยลดความผิดพลาดในการจัดการสถานะของโปรแกรม และทำให้การทำ Debugging เป็นเรื่องที่ง่ายขึ้นอย่างมหาศาล อย่างไรก็ตาม การใช้งานที่ผิดวิธีอาจนำไปสู่ปัญหา Performance หรือข้อผิดพลาดที่หาจุดแก้ได้ยาก

บทความนี้จะเจาะลึกถึงวิธีการใช้งาน Async/Await อย่างถูกต้อง ตั้งแต่พื้นฐานไปจนถึงเทคนิคขั้นสูง เพื่อให้คุณสามารถนำไปประยุกต์ใช้ในโปรเจกต์จริงได้อย่างมืออาชีพ โดยครอบคลุมทั้งเรื่องการจัดการ Error, การเพิ่มประสิทธิภาพด้วยการรันแบบขนาน และข้อควรระวังที่มักจะทำผิดกันบ่อยๆ

1. ทำความเข้าใจกลไกของ Async และ Await อย่างถ่องแท้

JavaScript Async/Await ใช้ยังไงให้ถูก

ก่อนที่จะเริ่มเขียนโค้ด เราต้องเข้าใจก่อนว่า async และ await ทำงานอย่างไรเบื้องหลัง เมื่อเราประกาศฟังก์ชันด้วยคีย์เวิร์ด async ฟังก์ชันนั้นจะถูกกำหนดให้ส่งคืนค่าเป็น Promise เสมอ ไม่ว่าเราจะ return ค่าเป็นตัวเลข สตริง หรืออ็อบเจกต์ธรรมดา JavaScript จะทำการ Wrap ค่านั้นไว้ใน Promise.resolve() โดยอัตโนมัติ

ส่วนคีย์เวิร์ด await จะใช้ได้เฉพาะภายในฟังก์ชันที่เป็น async เท่านั้น หน้าที่ของมันคือการสั่งให้ JavaScript Engine “หยุดรอ” จนกว่า Promise นั้นจะทำงานเสร็จสิ้น (settled) ไม่ว่าจะเป็นสถานะ resolved หรือ rejected ก็ตาม แต่การ “รอ” นี้ไม่ได้หมายถึงการหยุดการทำงานของ Thread หลัก (Main Thread) ทั้งหมด แต่มันเป็นการปล่อยให้ Event Loop ไปทำงานอื่นที่ค้างอยู่ในคิวแทน ทำให้แอปพลิเคชันของเรายังคงตอบสนองได้ (Responsive)

พื้นฐานการประกาศและใช้งาน

การใช้ Async/Await ช่วยให้โครงสร้างของโค้ดดูสะอาดตาขึ้นมาก ลองเปรียบเทียบกับการใช้ .then() ที่ต้องมีฟังก์ชันซ้อนอยู่ข้างใน การใช้ await ช่วยให้เราเก็บค่าที่ได้จาก Promise ลงในตัวแปรได้โดยตรงเหมือนการเขียนโปรแกรมปกติ

การทำงานร่วมกับ Event Loop

สิ่งสำคัญที่ต้องตระหนักคือ await จะทำการหยุดเฉพาะ Execution ของฟังก์ชันนั้นๆ เท่านั้น เมื่อ JavaScript เจอคำสั่ง await มันจะบันทึกสถานะของฟังก์ชันไว้ และย้ายฟังก์ชันนั้นออกจาก Call Stack ไปอยู่ใน Microtask Queue เพื่อรอให้ Promise ทำงานเสร็จสิ้น ซึ่งทำให้ JavaScript ยังสามารถจัดการกับการคลิกของผู้ใช้ หรือการ Render หน้าจอได้ตามปกติ

2. การจัดการ Error อย่างมืออาชีพด้วย Try-Catch

หนึ่งในข้อดีที่สุดของ Async/Await คือการที่เราสามารถใช้โครงสร้าง try…catch ซึ่งเป็นมาตรฐานในการจัดการ Error ของ JavaScript มาจัดการกับ Asynchronous Error ได้เลย ในขณะที่การใช้ Promises แบบเดิม เราต้องใช้ .catch() แยกออกมา ซึ่งบางครั้งทำให้ลำดับการไหลของ Error ดูสับสน

การใช้ try…catch ช่วยให้เราสามารถรวมกลุ่มการทำงานที่อาจเกิดข้อผิดพลาดไว้ด้วยกันได้ และจัดการกับ Exception ทุกรูปแบบที่เกิดขึ้นภายในบล็อกนั้น ไม่ว่าจะเป็น Network Error, Syntax Error ภายในฟังก์ชัน หรือการที่ Promise ถูก reject อย่างตั้งใจ การจัดการ Error ที่ดีควรมีการแยกประเภทของ Error เพื่อให้ผู้ใช้งานได้รับข้อความที่เหมาะสม และระบบยังคงทำงานต่อไปได้

โครงสร้างการจัดการ Error ที่ถูกต้อง

นอกจากการ catch error แล้ว เรายังสามารถใช้บล็อก finally เพื่อระบุคำสั่งที่ต้องการให้ทำงานเสมอ ไม่ว่าการทำงานจะสำเร็จหรือล้มเหลวก็ตาม เช่น การปิด Loading Spinner หรือการปิดการเชื่อมต่อฐานข้อมูล ซึ่งช่วยป้องกันปัญหา Memory Leak หรือ UI ค้างได้เป็นอย่างดี


async function fetchUserData(userId) {
    try {
        showLoading(true);
        const response = await fetch(`https://api.example.com/users/${userId}`);
        
        if (!response.ok) {
            throw new Error('ไม่สามารถดึงข้อมูลผู้ใช้ได้');
        }

        const data = await response.json();
        return data;
    } catch (error) {
        console.error('เกิดข้อผิดพลาด:', error.message);
        // สามารถจัดการ Error เฉพาะจุด หรือส่งต่อ Error ไปยัง Global Handler
        alert('ขออภัย เกิดข้อผิดพลาดในการโหลดข้อมูล');
    } finally {
        showLoading(false);
    }
}
    

การจัดการกับ Unhandled Rejections

แม้ว่าเราจะใช้ try…catch แต่บางครั้งเราอาจลืมครอบคลุมบางส่วนของโค้ด การมี Global Event Listener สำหรับ unhandledrejection จึงเป็นแนวทางปฏิบัติที่ดี (Best Practice) เพื่อบันทึก Log ของข้อผิดพลาดที่เราคาดไม่ถึงและป้องกันไม่ให้แอปพลิเคชันล่มโดยไม่มีสาเหตุ

3. Performance Optimization: อย่าตกหลุมพราง Sequential Execution

ข้อผิดพลาดที่พบบ่อยที่สุดในการใช้ Async/Await คือการเขียนโค้ดให้ทำงานแบบ Sequential (ทีละขั้นตอน) โดยไม่จำเป็น ตัวอย่างเช่น หากคุณต้องการดึงข้อมูลโปรไฟล์ผู้ใช้และรายการสินค้าพร้อมกัน หากคุณใส่ await ไว้หน้าการเรียกทั้งสองฟังก์ชันแยกกัน ตัวที่สองจะเริ่มทำงานก็ต่อเมื่อตัวแรกเสร็จสิ้นเท่านั้น

หากงานทั้งสองไม่ได้มีความเกี่ยวข้องกัน (เช่น ไม่ต้องใช้ ID จากโปรไฟล์ไปดึงสินค้า) การรันแบบนี้จะทำให้เสียเวลาโดยเปล่าประโยชน์ วิธีที่ถูกต้องคือการใช้ Promise.all() หรือ Promise.allSettled() เพื่อเริ่มการทำงานพร้อมกันในรูปแบบ Parallel ซึ่งจะช่วยลดเวลาการรอลงได้อย่างมหาศาล

การใช้ Promise.all เพื่อเพิ่มความเร็ว

Promise.all จะรับ Array ของ Promises และเริ่มทำงานทั้งหมดพร้อมกัน มันจะคืนค่ากลับมาเมื่อทุกตัวทำงานเสร็จสิ้น หากมีตัวใดตัวหนึ่งล้มเหลว มันจะ reject ทันที ซึ่งเหมาะสำหรับงานที่ต้องใช้ข้อมูลครบทุกส่วนจึงจะทำงานต่อได้


async function getDashboardData() {
    try {
        // เริ่มทำงานพร้อมกันทั้ง 3 อย่าง
        const profilePromise = fetchProfile();
        const postsPromise = fetchPosts();
        const settingsPromise = fetchSettings();

        // รอให้ทุกอย่างเสร็จสิ้น
        const [profile, posts, settings] = await Promise.all([
            profilePromise,
            postsPromise,
            settingsPromise
        ]);

        return { profile, posts, settings };
    } catch (error) {
        console.error('การดึงข้อมูล Dashboard ล้มเหลว', error);
    }
}
    

ความแตกต่างระหว่าง Promise.all และ Promise.allSettled

ในบางกรณี เราอาจต้องการให้งานอื่นๆ ทำงานต่อไปแม้ว่าจะมีบางงานล้มเหลว ในสถานการณ์นี้ Promise.allSettled จะตอบโจทย์กว่า เพราะมันจะรอให้ทุก Promise ทำงานจนจบ (ไม่ว่าจะสำเร็จหรือพัง) และคืนสถานะของแต่ละตัวกลับมาให้เราตรวจสอบภายหลัง

4. Async/Await ภายใน Loop และ Array Methods

การใช้ Async/Await ร่วมกับ Loop เป็นเรื่องที่ต้องระมัดระวังเป็นพิเศษ หลายคนพยายามใช้ .forEach() ร่วมกับ async callback แต่นั่นเป็นวิธีการที่ผิด เพราะ .forEach() ไม่ได้ถูกออกแบบมาให้รอ Promise ผลที่ได้คือ Loop จะรันจบไปก่อนที่งานข้างในจะเสร็จสิ้น

หากคุณต้องการให้การทำงานใน Loop เกิดขึ้นตามลำดับ (Sequential) คุณควรใช้ for…of loop แต่หากคุณต้องการให้งานใน Loop ทั้งหมดทำงานพร้อมกัน (Parallel) คุณควรใช้ .map() เพื่อสร้าง Array ของ Promises แล้วจึงใช้ Promise.all() ครอบอีกที

การใช้ For…of สำหรับงานที่ต้องทำทีละขั้นตอน

ตัวอย่างเช่น การอัปเดตฐานข้อมูลที่ละแถวโดยที่ลำดับมีความสำคัญ หรือเพื่อป้องกันไม่ให้ Server รับภาระหนักเกินไปจากการยิง Request จำนวนมากพร้อมกัน การใช้ for...of ร่วมกับ await จะช่วยควบคุมลำดับการทำงานได้อย่างแม่นยำ

การจัดการ Concurrency Limit

ในกรณีที่มีข้อมูลจำนวนมาก (เช่น 1,000 รายการ) การใช้ Promise.all เพื่อยิง Request พร้อมกันทั้งหมดอาจทำให้ Network คอขวด หรือโดน Rate Limit จาก API ได้ นักพัฒนาควรพิจารณาการใช้ Library อย่าง p-limit หรือการแบ่ง Chunk ข้อมูลเพื่อจำกัดจำนวนงานที่ทำพร้อมกัน (Concurrency Control)

5. Best Practices และข้อควรระวังในการเขียนโค้ด

การเขียน Clean Code ด้วย Async/Await ไม่ใช่แค่การทำให้มันทำงานได้ แต่คือการทำให้มันบำรุงรักษาได้ง่ายและมีประสิทธิภาพ หนึ่งในกฎเหล็กคือ “อย่าลืม await” เพราะถ้าคุณลืมใส่ await ผลลัพธ์ที่ได้จะเป็นอ็อบเจกต์ Promise แทนที่จะเป็นข้อมูลที่คุณต้องการ ซึ่งมักจะนำไปสู่ข้อผิดพลาดประเภท undefined ในบรรทัดถัดไป

นอกจากนี้ การใช้ async กับทุกฟังก์ชันพร่ำเพรื่อ (Over-using async) ก็อาจทำให้เกิด Overhead เล็กน้อยในระดับ Memory และประสิทธิภาพการประมวลผล หากฟังก์ชันนั้นไม่ได้มีการเรียกใช้ Asynchronous API ใดๆ เลย ก็ไม่จำเป็นต้องประกาศเป็น async

หลีกเลี่ยงการใช้ Async ใน Constructor

ใน JavaScript constructor ของ Class ไม่สามารถเป็น async ได้ หากคุณจำเป็นต้องทำการ Initialization ข้อมูลแบบ Asynchronous เมื่อสร้าง Instance แนะนำให้ใช้ Static Factory Method หรือแยกฟังก์ชัน init() ออกมาเรียกใช้งานหลังจากสร้าง Object เสร็จแล้ว

สรุปแนวทางการเขียนที่ถูกต้อง

  • ใช้ try…catch เสมอ เพื่อจัดการกับ Error ที่คาดไม่ถึง
  • ใช้ Promise.all เมื่อต้องการรันงานหลายอย่างที่ไม่เกี่ยวข้องกันพร้อมกัน
  • หลีกเลี่ยง .forEach เมื่อทำงานกับ Async ให้เปลี่ยนไปใช้ for...of หรือ .map() แทน
  • Return Promise โดยตรง หากฟังก์ชันนั้นเป็นฟังก์ชันสุดท้ายที่จะถูกเรียก เพื่อลด Overhead ของการสร้าง async wrapper
  • ระวังเรื่อง Scope ของตัวแปรเมื่อทำงานใน Loop เพื่อป้องกันปัญหา Data Race

สรุป

การใช้งาน JavaScript Async/Await อย่างถูกวิธีเป็นทักษะที่แยกนักพัฒนาระดับทั่วไปออกจากระดับมืออาชีพ มันไม่ใช่เพียงแค่การเปลี่ยนหน้าตาของโค้ดให้อ่านง่ายขึ้น แต่คือการเข้าใจจังหวะการทำงานของ Event Loop และการจัดการทรัพยากรของระบบอย่างมีประสิทธิภาพ การเลือกใช้ระหว่างการรันแบบทีละขั้นตอน (Sequential) และแบบขนาน (Parallel) รวมถึงการจัดการข้อผิดพลาดอย่างเป็นระบบ จะช่วยให้แอปพลิเคชันของคุณมีความเสถียร รวดเร็ว และง่ายต่อการขยายผลในอนาคต

Leave a Reply

Your email address will not be published. Required fields are marked *