Separate asynchronous scheduling from synchronous events
The event loop lets Node process callbacks when asynchronous work becomes ready. JavaScript on the main thread still runs one call at a time. Some operations use operating-system facilities or a worker pool; that does not mean every JavaScript function executes in parallel. A large synchronous scoring loop can delay all other requests served by that process.
Observe a reliable scheduling relationship
// scheduling.mjs
console.log('A: current script');
setTimeout(() => console.log('D: timer'), 0);
Promise.resolve().then(() => console.log('C: promise reaction'));
console.log('B: current script ends');
Run node scheduling.mjs. This example prints A, B, C, D. The current script finishes before its promise reaction, and that reaction runs before this timer callback. Do not generalize this into a universal ordering for every timer, I/O callback, setImmediate and process.nextTick combination. Their context and module format matter. A timer delay is a threshold, not an exact execution time.
EventEmitter listeners run during emit()
// events.mjs
import { EventEmitter } from 'node:events';
const exam = new EventEmitter();
function showResult(result) {
console.log('Result:', result.correct, '/', result.total);
}
exam.on('submitted', showResult);
exam.once('submitted', () => console.log('First submission recorded'));
exam.on('error', error => console.error('Exam event:', error.message));
console.log('Before emit');
exam.emit('submitted', { correct: 8, total: 10 });
console.log('After emit');
exam.emit('submitted', { correct: 9, total: 10 });
exam.off('submitted', showResult);
The first output is Before emit, followed by Result: 8 / 10, First submission recorded, and After emit. The second emit prints only Result: 9 / 10. on attaches a persistent listener; once removes its listener after the first event; off removes a particular function reference. Register listeners before emitting.
Error and lifecycle decisions
An emitted error event without an error listener throws and can terminate the process. A normal emitter does not await promises returned by async listeners. Handle asynchronous failures deliberately, or use an appropriate rejection-capture design. Adding a listener on every HTTP request without removing it can leak memory. A max-listener warning deserves investigation, not simply a higher limit.
An in-memory event is neither a durable queue nor proof of a database commit. If a notification must survive a process crash, use persistent job storage and retry rules. For CPU-heavy work, consider worker threads or a background job rather than recursively scheduling more callbacks.
Practice checkpoint
Add a named listener that increments a submission counter, remove it, and emit again to prove removal works. Predict the output before running. Then explain why sending an email from an event listener needs explicit error handling and why it should not determine whether an already committed exam result exists.