Every language
12 languages, copy-ready. One at a time with syntax highlighting, or all inline.
JSJavaScript
function debounce(fn, ms) {
let timer; // the state that must survive calls
function debounced(...args) {
clearTimeout(timer); // drop the pending fire
timer = setTimeout(() => fn(...args), ms); // rearm from NOW
}
debounced.cancel = () => clearTimeout(timer);
return debounced;
}
const save = debounce(() => api.save(draft), 300);
input.addEventListener('input', save);
// teardown (unmount): save.cancel();The timer id held in the closure IS the debounce — recreate the wrapper per event and every call fires. cancel() is the unmount companion; flush() (fire immediately, pending or not) and a leading:true variant (fire on the FIRST call, then stay quiet until silence) are the standard extensions.
TSTypeScript
function debounce<F extends (...args: any[]) => void>(
fn: F,
ms: number,
) {
let timer: ReturnType<typeof setTimeout> | undefined;
const debounced = (...args: Parameters<F>) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
debounced.cancel = () => clearTimeout(timer);
return debounced;
}Parameters<F> carries the argument types through so call sites stay checked — spelling args unknown[] would NOT be assignable to fn(...). ReturnType<typeof setTimeout> is the honest timer type: the DOM numbers timers, Node hands back a Timeout object.
GoGo
import (
"sync"
"time"
)
type Debouncer struct {
mu sync.Mutex
timer *time.Timer // the surviving state
}
// each call drops the pending fire and rearms it at now+ms:
func (d *Debouncer) call(fn func(), ms time.Duration) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
}
d.timer = time.AfterFunc(ms, fn)
}
func (d *Debouncer) cancel() {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
d.timer = nil
}
}The Stop-then-AfterFunc swap is the whole pattern: each call cancels the pending fire and rearms a new one, under a mutex because timers are not goroutine-safe. In a pipeline the leaner shape is a goroutine owning one time.Timer and calling t.Reset on each send — no mutex, one timer. Either way fn runs on a timer goroutine, not the caller's.
RsRust
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
/// send(()) on the returned handle restarts the quiet window;
/// f fires only after `ms` passes with no pings.
fn debouncer<F: Fn() + Send + 'static>(ms: u64, f: F) -> mpsc::Sender<()> {
let (tx, rx) = mpsc::channel::<()>();
thread::spawn(move || {
while rx.recv().is_ok() {
let mut deadline = Instant::now() + Duration::from_millis(ms);
loop {
let remain = deadline.saturating_duration_since(Instant::now());
if remain.is_zero() {
break;
}
match rx.recv_timeout(remain) {
Ok(()) => deadline = Instant::now() + Duration::from_millis(ms),
Err(mpsc::RecvTimeoutError::Timeout) => break,
Err(mpsc::RecvTimeoutError::Disconnected) => return,
}
}
f(); // quiet for the full window
}
});
tx
}Rust has no stdlib timer, so debounce is runtime-shaped, not stdlib-shaped: this version parks a thread on recv_timeout and pushes the deadline out on every ping. Real code reaches for tokio::time::sleep inside a select! loop that restarts on each ping — the same state machine with an async runtime instead of a dedicated thread.
PHPPHP
// No timers in plain CLI PHP — and a while(true) { sleep(...); } poll is
// NOT debounce: it blocks its own worker and fires on its own schedule,
// not on quiet. The honest form is an event loop (ReactPHP):
$loop = React\EventLoop\Loop::get();
function makeDebounced($loop, callable $fn, float $ms): callable {
$timer = null;
return function () use (&$timer, $loop, $fn, $ms) {
if ($timer !== null) {
$loop->cancelTimer($timer); // clearTimeout
}
$timer = $loop->addTimer($ms / 1000, $fn); // setTimeout, rearm
};
}
$save = makeDebounced($loop, fn() => print("saved\n"), 300);
$save();
$save(); // restarts the 300ms window — one fire when it goes quiet
$loop->run();The language gap is the trap: without an event loop there is no timer to clear, and the sleep-loop poll people reach for is the wrong shape entirely. Inside ReactPHP this is clearTimeout/setTimeout verbatim — and the timer handle must live in a use-by-reference closure, PHP's only surviving-state slot here.
PyPython
import threading
def debounce(fn, ms):
timer = None
def debounced(*args):
nonlocal timer # the surviving state
if timer is not None:
timer.cancel() # drop the pending fire
timer = threading.Timer(ms / 1000, fn, args)
timer.start()
def cancel():
nonlocal timer
if timer is not None:
timer.cancel()
timer = None
debounced.cancel = cancel
return debouncedTimer.cancel only flags the run to be skipped — if the window already elapsed and the thread is inside fn, cancel returns False and that fire happens anyway. Each call spawns a thread: fine at UI rates (a few per second), wrong at high rates — past that, feed one worker thread via a queue instead.
C#C#
using System;
using System.Threading;
using System.Threading.Tasks;
sealed class Debouncer : IDisposable
{
private CancellationTokenSource? cts;
public void Debounce(TimeSpan wait, Action fire)
{
// swap in a fresh CTS, then cancel the old one — race-free:
var old = Interlocked.Exchange(ref cts, new CancellationTokenSource());
old?.Cancel();
old?.Dispose();
var token = cts.Token;
_ = Task.Run(async () =>
{
try { await Task.Delay(wait, token); }
catch (TaskCanceledException) { return; } // superseded
fire();
});
}
public void Dispose() { cts?.Cancel(); cts?.Dispose(); }
}The CancellationTokenSource swap is the race-free idiom: Interlocked.Exchange arms the new token and returns the old one to cancel, so a concurrent Debounce call cannot leave two live waits. The alternative — one System.Threading.Timer reset via Change(dueTime, InfiniteTimeSpan) — is cheaper but hands you a race on the reset itself unless the same lock guards every call.
JvJava
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
class Debouncer implements AutoCloseable {
private final ScheduledExecutorService exec =
Executors.newSingleThreadScheduledExecutor();
private final AtomicReference<ScheduledFuture<?>> pending = new AtomicReference<>();
void call(Runnable fn, long ms) {
// arm the replacement FIRST, then drop the old fire:
ScheduledFuture<?> old = pending.getAndSet(
exec.schedule(fn, ms, TimeUnit.MILLISECONDS));
if (old != null) old.cancel(false);
}
@Override public void close() {
ScheduledFuture<?> f = pending.getAndSet(null);
if (f != null) f.cancel(false);
exec.shutdownNow();
}
}The AtomicReference to the ScheduledFuture is the swap: getAndSet arms the replacement and hands you the old handle in one atomic step, so two racing calls cannot both end up cancelled. cancel(false) never interrupts a fire already running; without shutdownNow() the executor thread keeps the JVM alive.
SwSwift
@MainActor
final class Debouncer {
private var task: Task<Void, Never>?
private let wait: Duration
init(milliseconds: Int) { wait = .milliseconds(milliseconds) }
func call(_ fire: @escaping @MainActor () -> Void) {
task?.cancel() // drop the pending fire
task = Task { [wait] in
do {
try await Task.sleep(for: wait)
} catch {
return // cancelled while sleeping — superseded
}
fire()
}
}
deinit { task?.cancel() }
}Store the Task, cancel it, relaunch — but the swallow is the trap: try? Task.sleep eats the CancellationError and fire() still runs, so catch the throw and return instead. @MainActor on the class keeps both cancel and fire off data races with UI state; deinit cancels so a deallocated owner fires nothing.
KtKotlin
import kotlinx.coroutines.*
class Debouncer(private val scope: CoroutineScope) {
private var job: Job? = null // this one field IS the debounce
fun call(ms: Long, fn: suspend () -> Unit) {
job?.cancel() // drop the pending fire
job = scope.launch {
delay(ms) // quiet window — cancel kills mid-sleep
fn()
}
}
}
// runBlocking {
// val d = Debouncer(this)
// repeat(5) { d.call(100) { println("saved") } } // one fire
// }Structured cancellation makes this trivial next to Java's AtomicReference dance: cancelling the old Job aborts its delay() in place, so fn() is unreachable for superseded calls. The scope must outlive the quiet window — cancel the scope and every pending fire dies with it (that is the feature, not a leak).
RbRuby
def debounce(fn, ms)
generation = 0 # epoch counter — the surviving state
lambda do |*args|
generation += 1
mine = generation # capture MY epoch
Thread.new do
sleep(ms / 1000.0)
fn.call(*args) if mine == generation # only the latest call runs
end
end
endThe generation (epoch) pattern replaces timer cancellation: each call increments the counter and the thread fires only if its captured epoch is still the latest — anything older wakes up dead. It costs one short-lived thread per call (UI rates only); concurrent-ruby's Concurrent::ScheduledTask gives the cancel-handle form instead.
ZigZig
const std = @import("std");
const Thread = std.Thread;
/// One worker thread + condvar: each ping resets the deadline; fire()
/// runs only after quiet_ms passes with no pings.
const Debouncer = struct {
mutex: Thread.Mutex = .{},
cond: Thread.Condition = .{},
deadline: i64 = 0, // ms timestamp; 0 = idle
quiet_ms: i64,
fire: *const fn () void,
fn ping(d: *Debouncer) void {
d.mutex.lock();
defer d.mutex.unlock();
d.deadline = std.time.milliTimestamp() + d.quiet_ms; // reset
d.cond.signal(); // wake the waiter
}
fn run(d: *Debouncer) void {
d.mutex.lock();
defer d.mutex.unlock();
while (true) {
if (d.deadline == 0) {
d.cond.wait(&d.mutex); // idle — sleep until a ping
continue;
}
const now = std.time.milliTimestamp();
if (now >= d.deadline) {
d.deadline = 0;
d.mutex.unlock();
d.fire(); // run WITHOUT the lock or ping deadlocks
d.mutex.lock();
} else {
d.cond.timedWait(
&d.mutex,
@intCast((d.deadline - now) * std.time.ns_per_ms),
) catch {}; // Timeout → loop and re-check the clock
}
}
}
};No stdlib timer event loop, and std.time.sleep BLOCKS — the honest shape is one worker thread on a condition variable whose timedWait deadline every ping pushes out. The deadline field reset under the mutex IS the debounce state; the trap is firing while holding the lock, which deadlocks the next ping.