Debounce chuẩn chỉ hoãn thời điểm gọi; nó không biết gì về Promise. Ba vấn đề phát sinh khi hàm bên trong là async:
1. Mất giá trị trả về. debounce(fn) trả undefined, nên await debouncedSearch(term) vô nghĩa. Muốn await được thì bản debounce phải giữ một promise và resolve nó ở lần chạy thật:
function debounceAsync(fn, wait) {
let timer
let pending
return (...args) => {
clearTimeout(timer)
pending ??= Promise.withResolvers()
timer = setTimeout(async () => {
const p = pending
pending = null
try { p.resolve(await fn(...args)) } catch (e) { p.reject(e) }
}, wait)
return pending.promise
}
}2. Vẫn còn race. Debounce chỉ giảm số lần gọi. Hai lần gọi cách nhau hơn wait vẫn có thể chồng phản hồi — cần abort hoặc kiểm tra "request mới nhất".
3. Lỗi bị nuốt. Trong bản debounce thường, fn chạy trong callback setTimeout, nên try/catch ở nơi gọi không bắt được; rejection trở thành unhandled.
Phân biệt lại với throttle: debounce chờ người dùng ngừng thao tác rồi mới chạy (search input, auto-save), throttle đảm bảo chạy tối đa N lần trong một khoảng (scroll, resize, mousemove). Nhớ cleanup timer khi unmount, nếu không callback sẽ chạy trên component đã bị gỡ.