Warm-up · Activity 1 of 7
Warm-up from tech.javascript: in which order does this program print?
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');// B4.4 · ~31 min · Beginner
After this lesson you can schedule, repeat and cancel timers in Node.js, predict when setImmediate, process.nextTick and promise callbacks run, and decide which timers may keep your program running.
You will be able to
Warm-up · Activity 1 of 7
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');Predict · Activity 2 of 7
Promise.resolve().then(() => console.log('resolve'));
queueMicrotask(() => console.log('microtask'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');Practice · Activity 3 of 7
const timer = setTimeout(() => console.log('fired'), 300);
console.log(timer.hasRef());
timer.____();
console.log(timer.hasRef());
console.log('done');Practice · Activity 4 of 7
Practice · Activity 5 of 7
import {readFile} from 'node:fs';
readFile('main.js', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});Brain teaser · Activity 6 of 7
let n = 0;
setTimeout(() => console.log('timer sees', n), 0);
function spin() {
if (++n < 100000) process.nextTick(spin);
}
process.nextTick(spin);Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
Part 1 schedules a nextTick, a microtask and a promise callback from the top level of an ES module. Part 2 schedules all four kinds inside an I/O callback, where their order is fixed. Part 3 starts a one-second timer and unrefs it. Part 4 iterates setInterval from node:timers/promises three times. The program never schedules setTimeout(0) against setImmediate from the main module, whose order is not fixed.
main.js
import {readFile} from 'node:fs';
import {setInterval} from 'node:timers/promises';
// 1. The main module is an ES module: microtasks run before the next tick queue.
process.nextTick(() => console.log('main: nextTick'));
queueMicrotask(() => console.log('main: queueMicrotask'));
Promise.resolve().then(() => console.log('main: promise'));
console.log('main: sync code');
// 2. Inside an I/O callback the order is fixed.
readFile('main.js', () => {
setTimeout(() => console.log('io: setTimeout 0'), 0);
setImmediate(() => console.log('io: setImmediate'));
queueMicrotask(() => console.log('io: queueMicrotask'));
process.nextTick(() => console.log('io: nextTick'));
console.log('io: callback');
});
// 3. An unref'd timer does not keep the program alive.
setTimeout(() => console.log('never printed'), 1000).unref();
// 4. timers/promises: setInterval as an async iterator.
let ticks = 0;
for await (const label of setInterval(50, 'tick')) {
console.log(label, ++ticks);
if (ticks === 3) break; // break stops the interval
}
console.log('done; nothing keeps the loop alive');
Run it with
node main.jsOutput
main: sync code
main: queueMicrotask
main: promise
main: nextTick
io: callback
io: nextTick
io: queueMicrotask
io: setImmediate
io: setTimeout 0
tick 1
tick 2
tick 3
done; nothing keeps the loop aliveExercise 1 of 2
debounce(fn, ms) returns a function that waits: each call cancels the call scheduled before it and schedules fn again, ms milliseconds later, with the newest arguments. So quick calls lead to one call of fn, with the last arguments, after a quiet pause of ms. The starter calls fn at once, every time. Use setTimeout and clearTimeout. This part runs in the browser too.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Keep the Timeout in a variable outside the returned function: let timer;
In the returned function, clearTimeout(timer) first. clearTimeout with undefined does nothing, so the first call is fine.
Then timer = setTimeout(() => fn(...args), ms); the arrow function keeps the newest args.
One way to solve it. Yours can look different and still pass the checks.
// Returns a function that calls fn only after ms milliseconds without a new call,
// with the arguments of the last call.
export function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer); // cancels the call scheduled before, if any
timer = setTimeout(() => fn(...args), ms);
};
}
// Typing H, He, Hello quickly: only the last text should be saved.
const save = debounce((text) => console.log('saved:', text), 50);
save('H');
save('He');
save('Hello');
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Returns a function that calls fn only after ms milliseconds without a new call,
// with the arguments of the last call.
export function debounce(fn, ms) {
return (...args) => fn(...args); // calls fn at once, every time
}
// Typing H, He, Hello quickly: only the last text should be saved.
const save = debounce((text) => console.log('saved:', text), 50);
save('H');
save('He');
save('Hello');
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {debounce} from './main.js';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
test('three quick calls lead to one call, with the last arguments', async () => {
const calls = [];
const save = debounce((text) => calls.push(text), 30);
save('H');
save('He');
save('Hello');
await sleep(90);
assert.deepEqual(calls, ['Hello'], `fn was called with ${JSON.stringify(calls)}`);
});
test('fn is not called before ms have passed', async () => {
const calls = [];
const save = debounce((text) => calls.push(text), 30);
save('now?');
assert.equal(calls.length, 0, 'fn was called at once; it should wait');
await sleep(90);
assert.equal(calls.length, 1, `fn was called ${calls.length} times after the pause; expected once`);
});
test('calls with a pause between them are all made', async () => {
const calls = [];
const save = debounce((text) => calls.push(text), 30);
save('a');
await sleep(90);
save('b');
await sleep(90);
assert.deepEqual(calls, ['a', 'b'], `fn was called with ${JSON.stringify(calls)}`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 2
countdown(from, ms, log) logs from, from - 1, ... down to 1, one line every ms milliseconds, then 'liftoff'. Use setInterval from node:timers/promises in a for await loop, and break after 1 so that the interval stops. watchdog(ms, onTimeout) returns a timer that calls onTimeout after ms but does not keep the program running. Run node main.js, then node --test.
This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.
for await (const _ of setInterval(ms)) { ... } runs its body once every ms.
Log String(n), count n down, and break when it reaches 0; after the loop, log 'liftoff'.
setTimeout(...).unref() returns the same Timeout, so you can return it directly.
One way to solve it. Yours can look different and still pass the checks.
import {setInterval} from 'node:timers/promises';
// Logs from, from - 1, ..., 1, one line every ms milliseconds, then 'liftoff'.
export async function countdown(from, ms, log = console.log) {
let n = from;
for await (const _ of setInterval(ms)) {
log(String(n));
n--;
if (n === 0) break; // break stops the interval
}
log('liftoff');
}
// A timer that calls onTimeout after ms, but does not keep the program running.
export function watchdog(ms, onTimeout) {
return setTimeout(onTimeout, ms).unref();
}
const guard = watchdog(2000, () => console.log('too slow'));
await countdown(3, 100);
console.log('guard keeps the program alive:', guard.hasRef());
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
import {setInterval} from 'node:timers/promises';
// Logs from, from - 1, ..., 1, one line every ms milliseconds, then 'liftoff'.
export async function countdown(from, ms, log = console.log) {
log('liftoff'); // no countdown yet
}
// A timer that calls onTimeout after ms, but does not keep the program running.
export function watchdog(ms, onTimeout) {
return setTimeout(onTimeout, ms);
}
const guard = watchdog(2000, () => console.log('too slow'));
await countdown(3, 100);
console.log('guard keeps the program alive:', guard.hasRef());
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {countdown, watchdog} from './main.js';
test('counts down from 3, then liftoff', async () => {
const log = [];
await countdown(3, 10, (line) => log.push(line));
assert.deepEqual(log, ['3', '2', '1', 'liftoff'], `the log is ${JSON.stringify(log)}`);
});
test('nothing is logged before the first interval has passed', async () => {
const log = [];
const done = countdown(2, 30, (line) => log.push(line));
assert.deepEqual(log, [], `right after the call, the log is already ${JSON.stringify(log)}`);
await done;
assert.deepEqual(log, ['2', '1', 'liftoff'], `the log is ${JSON.stringify(log)}`);
});
test('the watchdog does not keep the program running', () => {
const timer = watchdog(10000, () => {});
const kept = timer.hasRef();
clearTimeout(timer);
assert.equal(kept, false, 'hasRef() should be false: call unref() on the timer');
});
test('the watchdog still fires while the program is running', async () => {
const got = await new Promise((resolve) => {
watchdog(20, () => resolve('fired'));
setTimeout(resolve, 300, 'not fired');
});
assert.equal(got, 'fired', 'onTimeout should be called after ms');
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsfunction save() {
console.log('saved');
}
setTimeout(save(), 100);
What Node.js prints
TypeError [ERR_INVALID_ARG_TYPE]: The "callback" argument must be of type function. Received undefinedWhy, and the fix
save() runs at once and prints saved; setTimeout then receives its return value, undefined, instead of a function, and throws. Pass the function itself, setTimeout(save, 100), or wrap the call: setTimeout(() => save(), 100).
console.log('waiting');
await setTimeout(100);
console.log('done');
What Node.js prints
TypeError [ERR_INVALID_ARG_TYPE]: The "callback" argument must be of type function. Received type number (100)Why, and the fix
Without an import, setTimeout is the global callback version: its first argument must be a function, so 100 is rejected. The promise version lives in node:timers/promises: import {setTimeout} from 'node:timers/promises'; then await setTimeout(100) waits 100 ms.
import {setTimeout} from 'node:timers/promises';
const later = setTimeout(5000, 'late');
later.unref();
console.log(await later);
What Node.js prints
TypeError: later.unref is not a functionWhy, and the fix
setTimeout from node:timers/promises returns a Promise, not a Timeout, and a Promise has no unref method. Pass the option instead: setTimeout(5000, 'late', {ref: false}). Keep in mind that with nothing else pending, the program may then exit before the promise settles.
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.