ข้ามไปยังเนื้อหา

Timers

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(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(fn, 0) ไม่ได้ หมายถึง “รันทันที” แต่จัดคิว fn เป็น macrotask microtask ที่ค้างอยู่ (callback ของ Promise, queueMicrotask) จะทำงานก่อน

setTimeout(function() { console.log('Timer'); }, 0);
Promise.resolve().then(function() { console.log('Promise'); });
// Output: Promise, then Timer

หากต้องการจัดคิว 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 ระบายก่อน

JavaScript

ผลลัพธ์จะเป็นเสมอ: Promise ran first, แล้ว Timer ran second

node:timers/promises ให้ setTimeout แบบที่ return Promise — ไม่มี callback จึงเข้ากับ await, Promise.race และ AbortSignal ได้เป็นธรรมชาติ การส่ง { signal } ให้คุณยกเลิกการรอก่อนกำหนดได้แทนที่จะปล่อยให้รอจนครบ:

Node.js
Output (verified on Node 24)
waiting 30ms with timers/promises...
done waiting
aborted: AbortError

AbortSignal.timeout(ms) คืน signal ที่ abort ตัวเองหลังผ่านไป ms — วิธีสั้น ๆ ในการบอกว่า “ยอมแพ้หลังจากนานขนาดนี้” โดยไม่ต้องประกอบ setTimeout + AbortController เองทุกครั้ง ใช้ได้ทุกที่ที่รับ signal: timers/promises, fetch และโค้ดของคุณเองที่เช็ก signal.aborted หรือฟัง 'abort'

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

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 สร้างบางครั้งใช้ 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); })

setTimeout(fn, 0) รับประกันหรือไม่ว่าจะรัน fn ทันทีหลังโค้ด synchronous ปัจจุบัน?
คุณหยุด setInterval ไม่ให้ทำงานอีกได้อย่างไร?
ฟังก์ชันใดจัดคิว callback ใน microtask queue (ไม่ใช่ timer queue)?
โค้ดนี้พิมพ์อะไร?
import { setTimeout as delay } from 'node:timers/promises';
try {
  await delay(1000, undefined, { signal: AbortSignal.timeout(10) });
  console.log('finished');
} catch (err) {
  console.log('caught:', err.name);
}
ฟังก์ชันที่ AI เขียนเรียก setTimeout(() => console.log(user.name), 0) ทันทีหลังเรียก fetchUser().then(u => user = u) พร้อม comment ว่า "รันในรอบถัดไป ทันทีหลัง fetch ด้านบน" เหตุผลนี้ผิดตรงไหน?