Timers
setTimeout
หัวข้อที่มีชื่อว่า “setTimeout”setTimeout(fn, delay) จัดคิวให้ fn ทำงานเป็น macrotask หลังจากผ่านไปอย่างน้อย delay มิลลิวินาที ค่า delay เป็น ค่าต่ำสุด — ไม่ใช่การรับประกัน ถ้า call stack ไม่ว่างตอน timer ทำงาน callback จะรอจนกว่า stack จะว่าง
Node จะปัดค่า delay ที่ต่ำกว่า 1 ms ขึ้นเป็น 1 ms เสมอ — ไม่ว่าจะใส่ 0 หรือค่าติดลบ ก็จะทำงานหลังจากประมาณ 1 ms ทั้งคู่ (ค่าติดลบยังขึ้น TimeoutNegativeWarning ด้วย) ตัวเลข “ขั้นต่ำ 4 ms” ที่มักอ้างถึงเป็นกฎของ เบราว์เซอร์ สำหรับ timer ที่ซ้อนลึกตั้งแต่ 5 ชั้นขึ้นไป — ไม่ใช้กับ setTimeout ของ Node
var id = setTimeout(function() { console.log('Runs after at least 100 ms');}, 100);
// Cancel before it fires:clearTimeout(id);setInterval
หัวข้อที่มีชื่อว่า “setInterval”setInterval(fn, delay) จัดคิวให้ fn ซ้ำ ๆ ทุก delay ms ใช้ clearInterval เพื่อหยุด
var count = 0;var id = setInterval(function() { count++; console.log('tick', count); if (count >= 3) clearInterval(id);}, 500);setTimeout ที่มี delay เป็น 0
หัวข้อที่มีชื่อว่า “setTimeout ที่มี delay เป็น 0”setTimeout(fn, 0) ไม่ได้ หมายถึง “รันทันที” แต่จัดคิว fn เป็น macrotask microtask ที่ค้างอยู่ (callback ของ Promise, queueMicrotask) จะทำงานก่อน
setTimeout(function() { console.log('Timer'); }, 0);Promise.resolve().then(function() { console.log('Promise'); });// Output: Promise, then TimerqueueMicrotask
หัวข้อที่มีชื่อว่า “queueMicrotask”หากต้องการจัดคิว callback ให้ทำงานก่อน macrotask ถัดไป ใช้ queueMicrotask:
queueMicrotask(function() { console.log('This runs in the microtask queue, before any timer');});เดโมที่รันได้
หัวข้อที่มีชื่อว่า “เดโมที่รันได้”เดโมนี้แสดงว่า setTimeout(fn, 0) ยังทำงาน หลัง .then ของ Promise ที่ resolve แล้ว เพราะ callback ของ timer เป็น macrotask และ microtask ระบายก่อน
ผลลัพธ์จะเป็นเสมอ: Promise ran first, แล้ว Timer ran second
Timer แบบ Promise: timers/promises กับ AbortSignal.timeout
หัวข้อที่มีชื่อว่า “Timer แบบ Promise: timers/promises กับ AbortSignal.timeout”node:timers/promises ให้ setTimeout แบบที่ return Promise — ไม่มี callback จึงเข้ากับ await, Promise.race และ AbortSignal ได้เป็นธรรมชาติ การส่ง { signal } ให้คุณยกเลิกการรอก่อนกำหนดได้แทนที่จะปล่อยให้รอจนครบ:
Output (verified on Node 24)
waiting 30ms with timers/promises...
done waiting
aborted: AbortErrorAbortSignal.timeout(ms) คืน signal ที่ abort ตัวเองหลังผ่านไป ms — วิธีสั้น ๆ ในการบอกว่า “ยอมแพ้หลังจากนานขนาดนี้” โดยไม่ต้องประกอบ setTimeout + AbortController เองทุกครั้ง ใช้ได้ทุกที่ที่รับ signal: timers/promises, fetch และโค้ดของคุณเองที่เช็ก signal.aborted หรือฟัง 'abort'
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”API ของ timer ทุกตัวคือคำสัญญาเรื่องการรออย่างน้อยเท่านั้น ไม่เคยรอแบบเป๊ะ: setTimeout/setInterval ส่ง callback ให้ timer phase ของ libuv ซึ่งจะทำงานก็ต่อเมื่อ call stack ว่างและทุก microtask ระบายหมดแล้ว — ค่า delay ที่คุณใส่คือพื้นล่าง ไม่ใช่ตารางเวลาที่แน่นอน setTimeout ของ timers/promises คือ timer ตัวเดิมที่ห่อด้วย Promise แทน callback ซึ่งเป็นเหตุผลว่าทำไมมันยกเลิกได้ด้วย signal (callback ไม่มีทางบอกว่า “ไม่เอาแล้ว” แต่ Promise reject ได้) — AbortSignal.timeout(ms) ก็แค่ signal สำเร็จรูปที่ abort ตัวเองหลังผ่านไป ms จึงไม่ต้องสร้าง AbortController พร้อม setTimeout ของตัวเองทุกครั้งที่ต้องการ deadline
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”Timer แบบ callback (setTimeout, setInterval) ไม่มีวิธีบอก “เลิกรอ” ในตัว — คุณต้องเก็บ ID เองแล้วเรียก clearTimeout/clearInterval และไม่มีอะไรเทียบเท่ากับ “หยุดรอสิ่งนี้ให้ settle” เมื่อ callback ยิงไปแล้ว API แบบ Promise ของ timers/promises มีต้นทุนแค่ import เพิ่มหนึ่งตัวกับ signal object แต่แลกมาด้วยการยกเลิกได้จริงและเข้ากับ Promise.race/Promise.all ตรง ๆ — คุ้มสำหรับอะไรก็ตามที่มี deadline จริง (การเรียก upstream, งานที่ต้องไม่รันตลอดไป) และเกินความจำเป็นสำหรับ callback แบบ “รันทีหลังเฉย ๆ” ที่ไม่ต้องเปลี่ยนใจ
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “setTimeout(fn, 0) รันก่อนสิ่งที่ต่อคิวไว้ทุกอย่าง” มันรันก่อน macrotask ถัดไป เท่านั้น แต่รันหลัง microtask ที่ค้างอยู่ทุกตัวเสมอ (
process.nextTick, Promise reaction,queueMicrotask) — ดูเดโมด้านบนที่ Promise พิมพ์ก่อนเสมอ - “setTimeout(fn, 0) กับ process.nextTick(fn) แทบจะเหมือนกัน” สองอย่างนี้อยู่คนละคิวเลย:
nextTick(และ microtask) ระบายจนหมดหลังจากทุก callback เดี่ยว ส่วนsetTimeout(fn, 0)รอรอบเต็มของ timers phase ถัดไป ซึ่งอาจช้ากว่าหลาย callback ถ้าระบบทำงานหนัก - “delay เป็น 0 หรือติดลบแปลว่า callback รันแบบ synchronous” ไม่เคยเป็นแบบนั้น — แม้แต่
setTimeout(fn, -100)ก็ถูกปัดขึ้นเป็นพื้น ~1 ms ของ Node และยังคืนการควบคุมให้ event loop ก่อนเสมอ ไม่มีอะไรที่ตั้งด้วยsetTimeoutรันบน stack frame ปัจจุบันเลย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ดที่ AI สร้างบางครั้งใช้ setTimeout(fn, 0) เป็นตัวแทนแบบไม่เป็นทางการของ “รอบถัดไป” หรือ “ทันทีหลังงาน async นี้เสร็จ” — ปฏิบัติกับมันเหมือนเป็นกลไก synchronization ทั้งที่ไม่ได้ถูกออกแบบมาแบบนั้นเลย
🐛 หา bug ในโค้ดนี้
function loadUserThenGreet(fetchUser) {let user = null;fetchUser().then(function (u) { user = u; });
setTimeout(function () { // AI comment: "setTimeout(fn, 0) runs on the next tick, right after the fetch above" console.log('Hello, ' + user.name);}, 0);}เฉลย
setTimeout(fn, 0) เป็น macrotask ที่มีพื้นประมาณ 1 ms — มันไม่เกี่ยวข้องอะไรเลยกับเวลาที่ fetchUser() ใช้จริง ถ้า fetchUser() ทำ I/O จริง (network call, database query) user แทบจะยังเป็น null แน่ ๆ ตอนที่ timeout ทำงาน แล้ว user.name ก็ throw TypeError แม้แต่การเรียกสิ่งนี้ว่า “รอบถัดไป” ก็ผิดอยู่ดี: process.nextTick กับ Promise microtask เป็นคิวคนละคิวที่ระบายก่อน ไม่เกี่ยวอะไรกับ macrotask queue ของ setTimeout เลย วิธีแก้คือตอบสนองต่อ Promise จริง ๆ แทนการเดา delay: fetchUser().then(function (u) { console.log('Hello, ' + u.name); })