Error Handling — Tricky Interview Questions
Q1: uncaughtException — Should You Use It?
javascriptprocess.on('uncaughtException', (err, origin) => {
console.error('Uncaught exception:', err.message);
// Is this safe to continue after?
});
setTimeout(() => {
throw new Error('boom');
}, 100);Answer: You can catch it, but you should NOT continue running. After an uncaught exception, the application is in an unknown state (partial operations, corrupted memory, open file handles). The safe pattern:
javascriptprocess.on('uncaughtException', (err) => {
// 1. Log the error
console.error('FATAL - uncaughtException:', err);
// 2. Try to close resources gracefully
server.close(() => {
// 3. Exit with error code
process.exit(1);
});
// 4. Forced exit if cleanup takes too long
setTimeout(() => process.exit(1), 5000).unref();
});Q2: unhandledRejection Behavior by Version
javascriptasync function fail() {
throw new Error('async error');
}
fail(); // No .catch() or await!
// Node.js v14: Warning logged, process continues
// Node.js v15+: Process exits with code 1 by default!
// --unhandled-rejections flag controls behaviorHandle globally:
javascriptprocess.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
process.exit(1); // recommended
});Q3: try/catch with async — What Does It Catch?
javascriptasync function test() {
try {
// Case 1: await — CAUGHT
await Promise.reject(new Error('awaited rejection'));
// Case 2: sync throw — CAUGHT
throw new Error('sync throw');
// Case 3: setTimeout — NOT CAUGHT
setTimeout(() => { throw new Error('timer'); }, 0);
// Case 4: unawaited promise — NOT CAUGHT
Promise.reject(new Error('unawaited')); // unhandled rejection!
} catch (e) {
console.log('caught:', e.message);
}
}Only awaited Promises and synchronous throws are caught by try/catch.
Q4: Error in Promise Chain
javascriptPromise.resolve()
.then(() => { throw new Error('from then'); })
.catch(e => {
console.log('caught:', e.message);
throw new Error('from catch'); // rethrowing!
})
.then(() => console.log('after catch'))
.catch(e => console.log('second catch:', e.message));Output:
caught: from then
second catch: from catchIf .catch() re-throws, the error propagates to the next .catch(). .then() after the first .catch() is skipped.
Q5: Error vs throw string
javascripttry {
throw 'string error'; // string, not Error object
} catch (e) {
console.log(e instanceof Error); // ?
console.log(typeof e); // ?
console.log(e.stack); // ?
}Answer: false, 'string', undefined
Why: You can throw anything in JS — strings, numbers, objects. But you lose the stack trace! Always throw Error objects:
javascriptthrow new Error('message'); // has .stack, .message, .nameQ6: Domain Module vs AsyncLocalStorage
javascript// domains (deprecated) — old way to associate context with async:
const domain = require('domain');
const d = domain.create();
d.on('error', (err) => console.error('domain caught:', err));
d.run(() => {
setTimeout(() => { throw new Error('in domain'); }, 100);
// Domain catches it!
});
// Modern alternative — AsyncLocalStorage for context propagation:
const { AsyncLocalStorage } = require('async_hooks');
const requestContext = new AsyncLocalStorage();
app.use((req, res, next) => {
requestContext.run({ requestId: uuid() }, next);
});
// Anywhere in the async chain — access the context:
const ctx = requestContext.getStore();
console.log(ctx.requestId); // same ID throughout the request lifecycleQ7: Error Propagation in Streams
javascriptconst readable = fs.createReadStream('nonexistent.txt');
const writable = fs.createWriteStream('output.txt');
// ❌ pipe doesn't forward errors!
readable.pipe(writable);
// readable errors don't propagate to writable
// writable stays open — file handle leak!
// ✅ pipeline handles errors:
pipeline(readable, writable, (err) => {
if (err) {
console.error('Pipeline failed:', err.message);
// both streams are destroyed automatically
}
});Q8: gracefulExit Pattern
javascriptlet isShuttingDown = false;
async function gracefulShutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`\nReceived ${signal}. Graceful shutdown...`);
// 1. Stop accepting new connections
server.close();
// 2. Wait for in-flight requests to complete
// (you'd typically track these)
// 3. Close database connections
await db.end();
// 4. Close message queue consumers
await consumer.disconnect();
console.log('Cleanup complete, exiting');
process.exit(0);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('uncaughtException', (err) => {
console.error('Uncaught:', err);
gracefulShutdown('uncaughtException').then(() => process.exit(1));
});Q9: Error instanceof Check
javascriptclass DatabaseError extends Error {}
class ConnectionError extends DatabaseError {}
const err = new ConnectionError('db down');
console.log(err instanceof Error); // ?
console.log(err instanceof DatabaseError); // ?
console.log(err instanceof ConnectionError); // ?
console.log(err.name); // ?Answer: true, true, true, 'ConnectionError'
Custom errors work with instanceof through the prototype chain. err.name is automatically set to the class name when you extend Error (if you don't override the constructor without calling super()).
Q10: try/catch with synchronous EventEmitter
javascriptconst { EventEmitter } = require('events');
const ee = new EventEmitter();
ee.on('error', (err) => {
console.log('error event caught:', err.message);
});
try {
ee.emit('error', new Error('emitted error'));
console.log('after emit');
} catch (e) {
console.log('try/catch caught:', e.message); // does this run?
}Answer: 'error event caught: emitted error' then 'after emit'
Since there's an 'error' event listener, the error is handled by it (not thrown). try/catch doesn't catch it. If there were NO 'error' listener, emit('error', err) WOULD throw and the catch WOULD fire.