30 complete questions with solutions, explanations, and code for TCS AI Career exam preparation
Express • Async/Await • Streams • CRUD • EventEmitter • MiddlewareFundamentals of creating Express servers, routing, and returning JSON responses.
Create an Express app that:
3000GET / → returns { "message": "Hello TCS" } with status 200{ "error": "Not Found" } with status 404const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.status(200).json({ message: 'Hello TCS' });
});
// Catch-all for any other route
app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
app.get('/') handles only GET requests to the root. The app.use() at the end acts as a catch-all — any request that doesn't match a previous route falls into it. Order matters: the catch-all must come after all defined routes.
Implement GET /health that returns:
{ "status": "ok", "timestamp": "<ISO string>" }
Use new Date().toISOString() for the timestamp.
const express = require('express');
const app = express();
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString()
});
});
app.listen(3000);
res.json() automatically sets Content-Type: application/json and serializes the object. new Date().toISOString() returns a string like "2026-07-17T10:30:00.000Z".
Build an Express app with:
GET /counter → returns current count (starts at 0)POST /counter/increment → increments by 1, returns new countPOST /counter/reset → resets to 0, returns new countconst express = require('express');
const app = express();
app.use(express.json());
let count = 0;
app.get('/counter', (req, res) => {
res.json({ count });
});
app.post('/counter/increment', (req, res) => {
count++;
res.json({ count });
});
app.post('/counter/reset', (req, res) => {
count = 0;
res.json({ count });
});
app.listen(3000);
count variable lives in process memory. Every time the server restarts, count resets to 0. This is fine for exam purposes — real apps would use a database. Note: POST is used for increment/reset because they change server state.
Reading files asynchronously, getting file stats, and streaming large files.
Write a Node script that reads input.txt asynchronously, logs its content, and handles errors (e.g., file not found).
const fs = require('fs');
fs.readFile('input.txt', 'utf8', (err, data) => {
if (err) {
if (err.code === 'ENOENT') {
console.error('Error: File "input.txt" not found.');
} else {
console.error('Error reading file:', err.message);
}
return;
}
console.log('File content:\n', data);
});
fs.readFile is callback-based and non-blocking. The first argument to the callback is the error object (null if success). err.code === 'ENOENT' checks specifically for "file not found". Always handle errors gracefully.
Create a function getFileStats(filePath) that uses fs.promises.stat and returns:
{ sizeInBytes: Number, createdTime: Date, modifiedTime: Date }
Also create an Express route GET /file-stats?path=... that returns this JSON.
const fs = require('fs').promises;
const express = require('express');
const app = express();
async function getFileStats(filePath) {
const stat = await fs.stat(filePath);
return {
sizeInBytes: stat.size,
createdTime: stat.birthtime,
modifiedTime: stat.mtime
};
}
app.get('/file-stats', async (req, res) => {
try {
const filePath = req.query.path;
if (!filePath) {
return res.status(400).json({ error: 'path query param is required' });
}
const stats = await getFileStats(filePath);
res.json(stats);
} catch (err) {
res.status(404).json({ error: 'File not found or inaccessible' });
}
});
app.listen(3000);
fs.promises.stat returns a Stats object. stat.size is in bytes, stat.birthtime is creation time, stat.mtime is last modified time. Using async/await with try/catch is cleaner than nested callbacks.
Implement GET /stream-file that streams a large file using fs.createReadStream, sets appropriate Content-Type, and handles stream errors.
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.get('/stream-file', (req, res) => {
const filePath = path.join(__dirname, 'large.txt');
const readStream = fs.createReadStream(filePath);
// Set headers before piping
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Disposition', 'attachment; filename="large.txt"');
readStream.pipe(res);
readStream.on('error', (err) => {
console.error('Stream error:', err.message);
if (!res.headersSent) {
res.status(404).json({ error: 'File not found' });
} else {
res.end();
}
});
});
app.listen(3000);
createReadStream reads the file in chunks (default 64KB), so memory usage stays constant regardless of file size. .pipe(res) connects the readable stream to the writable response stream. Always handle 'error' events on streams — unhandled stream errors crash the process.
Core asynchronous patterns in Node.js — promises, parallel/sequential execution, and event loop behavior.
Write a delay(ms) function that returns a promise resolving after ms milliseconds. Then use it to log "Start", wait 1s, log "After 1s", wait 2s, log "After 3s total".
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function main() {
console.log('Start');
await delay(1000);
console.log('After 1s');
await delay(2000);
console.log('After 3s total');
}
main();
setTimeout doesn't return a promise natively — we wrap it in a new Promise. The await keyword pauses the async function execution until the promise resolves, but does not block the event loop. Other code can still run while we wait.
Given ["a.txt", "b.txt", "c.txt"], read all files in parallel using Promise.all. Return an array of contents. If any file fails, reject the whole operation.
const fs = require('fs').promises;
async function readFilesParallel(filePaths) {
const promises = filePaths.map(path => fs.readFile(path, 'utf8'));
return Promise.all(promises);
}
// Usage
const files = ['a.txt', 'b.txt', 'c.txt'];
readFilesParallel(files)
.then(contents => {
contents.forEach((content, i) => {
console.log(`--- ${files[i]} ---`);
console.log(content);
});
})
.catch(err => {
console.error('One or more files failed to read:', err.message);
});
Promise.all takes an array of promises and resolves with an array of results (in the same order). If any promise rejects, Promise.all immediately rejects with that error. This is "fail-fast" behavior. Files are read concurrently — much faster than sequential for I/O-bound tasks.
Same as above but read files one by one in sequence using a loop and await.
const fs = require('fs').promises;
async function readFilesSequential(filePaths) {
const contents = [];
for (const filePath of filePaths) {
const content = await fs.readFile(filePath, 'utf8');
contents.push(content);
}
return contents;
}
// Usage
const files = ['a.txt', 'b.txt', 'c.txt'];
readFilesSequential(files)
.then(contents => console.log(contents))
.catch(err => console.error('Read failed:', err.message));
for...of with await reads each file only after the previous one finishes. This is slower for independent I/O but necessary when order matters or files depend on each other. Note: for...of works with await; .forEach does not because it doesn't await the callback.
Building complete CRUD APIs with validation, filtering, and error handling.
Build an Express app with in-memory users = []. Implement GET all, GET by id, POST, PUT, DELETE with validation (name & email required for POST/PUT).
const express = require('express');
const app = express();
app.use(express.json());
let users = [];
let nextId = 1;
// GET /users — return all
app.get('/users', (req, res) => {
res.json(users);
});
// GET /users/:id — return by id
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
// POST /users — create
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
const user = { id: nextId++, name, email };
users.push(user);
res.status(201).json(user);
});
// PUT /users/:id — update
app.put('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: 'User not found' });
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
user.name = name;
user.email = email;
res.json(user);
});
// DELETE /users/:id
app.delete('/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: 'User not found' });
users.splice(index, 1);
res.json({ message: 'Deleted' });
});
app.listen(3000);
parseInt(req.params.id) converts URL params (strings) to numbers. find() returns the first match; findIndex() returns its index (or -1). splice(index, 1) removes one element at that index. Status codes: 200 (OK), 201 (Created), 400 (Bad Request), 404 (Not Found).
Extend the users API so GET /users?role=admin returns only matching users. Support multiple filters like ?role=admin&active=true.
// Add this to the GET /users route from Q10
app.get('/users', (req, res) => {
let filtered = [...users]; // clone array
// Apply each query param as a filter
Object.keys(req.query).forEach(key => {
let value = req.query[key];
// Parse boolean strings
if (value === 'true') value = true;
else if (value === 'false') value = false;
filtered = filtered.filter(user => user[key] === value);
});
res.json(filtered);
});
req.query is an object of all query parameters (always strings from URL). We iterate over each filter key and narrow down results. Boolean strings ("true"/"false") are parsed to actual booleans. For number parsing, use Number(value). This pattern is reusable for any set of filters.
Add a global error handler and a route that intentionally throws an error to test it.
const express = require('express');
const app = express();
app.use(express.json());
// A route that throws an error
app.get('/crash', (req, res, next) => {
const err = new Error('Something broke on purpose!');
err.status = 500;
next(err); // pass error to error-handling middleware
});
// Global error handler — MUST have 4 parameters
app.use((err, req, res, next) => {
console.error('Error:', err.message);
res.status(err.status || 500).json({
error: 'Something went wrong',
message: err.message
});
});
app.listen(3000);
(err, req, res, next). If you omit err, Express treats it as regular middleware. Calling next(err) with an argument skips all normal middleware and goes straight to the error handler.
Writing custom middleware for logging, authentication, and rate limiting.
Write middleware that logs HTTP method, URL, and timestamp for every request, then calls next().
function requestLogger(req, res, next) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} ${req.originalUrl}`);
next();
}
// Apply globally
app.use(requestLogger);
// Or apply to specific routes
app.get('/users', requestLogger, (req, res) => {
res.json(users);
});
(req, res, next). Always call next() to pass control to the next middleware — otherwise the request hangs forever. req.originalUrl includes query strings; req.url doesn't if mounted on a sub-router.
Check for header Authorization: Bearer <token>. If token is "secret123", call next(). Otherwise respond 401. Apply only to GET /protected.
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Extract token from "Bearer <token>"
const token = authHeader.split(' ')[1];
if (token === 'secret123') {
next();
} else {
res.status(401).json({ error: 'Unauthorized' });
}
}
// Apply only to GET /protected
app.get('/protected', authMiddleware, (req, res) => {
res.json({ message: 'You have access to protected data!' });
});
// This route does NOT require auth
app.get('/public', (req, res) => {
res.json({ message: 'Anyone can see this' });
});
split(' ')[1] extracts the token after "Bearer ". In production, use JWT verification and environment variables for secrets — never hardcode them.
Track requests per IP. Allow max 5 requests per minute per IP. Return 429 if exceeded. Reset counts periodically.
const rateLimits = {}; // { ip: { count: N, resetTime: timestamp } }
function rateLimiter(req, res, next) {
const ip = req.ip;
const now = Date.now();
const windowMs = 60 * 1000; // 1 minute
const maxRequests = 5;
if (!rateLimits[ip] || now > rateLimits[ip].resetTime) {
// First request or window expired — start new window
rateLimits[ip] = { count: 1, resetTime: now + windowMs };
return next();
}
rateLimits[ip].count++;
if (rateLimits[ip].count > maxRequests) {
return res.status(429).json({ error: 'Too many requests' });
}
next();
}
app.use(rateLimiter);
app.get('/', (req, res) => {
res.json({ message: 'Hello' });
});
express-rate-limit with Redis backing.
Extending EventEmitter, emitting custom events, and async event processing.
Create OrderService extending EventEmitter. Method createOrder(data) emits "orderCreated". A listener logs the order id.
const { EventEmitter } = require('events');
class OrderService extends EventEmitter {
createOrder(data) {
// Do order creation logic here...
console.log('Creating order:', data);
this.emit('orderCreated', data);
}
}
// Usage
const orderService = new OrderService();
orderService.on('orderCreated', (data) => {
console.log(`Order created with id: ${data.id}`);
});
orderService.createOrder({ id: 123, item: 'Laptop' });
EventEmitter is a core Node.js class. .on(event, listener) registers a listener. .emit(event, data) triggers all listeners for that event. You can have multiple listeners for the same event — they run in registration order.
Make the event listener async (simulate DB save with await delay(500)). Ensure errors inside the listener don't crash the process.
const { EventEmitter } = require('events');
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class OrderService extends EventEmitter {
createOrder(data) {
this.emit('orderCreated', data);
}
}
const orderService = new OrderService();
// Async listener with error handling
orderService.on('orderCreated', async (data) => {
try {
console.log(`Processing order ${data.id}...`);
await delay(500); // simulate DB save
console.log(`Order ${data.id} saved to database.`);
} catch (err) {
console.error(`Error processing order ${data.id}:`, err.message);
// Don't re-throw — prevents process crash
}
});
// Also handle uncaught errors on the emitter
orderService.on('error', (err) => {
console.error('OrderService error:', err.message);
});
orderService.createOrder({ id: 456, item: 'Phone' });
// Intentionally trigger error to show error handler
// Uncomment to test:
// orderService.on('orderCreated', async () => { throw new Error('DB down!'); });
// orderService.createOrder({ id: 789, item: 'Tablet' });
async listeners with EventEmitter, the emitter doesn't await them — errors become unhandled promise rejections. Always wrap async listener bodies in try/catch. The 'error' event on EventEmitter is special: if no listener is registered, it throws and crashes the process.
Pure JavaScript utility functions commonly asked in coding rounds.
Implement a debounce(fn, delay) that delays calling fn until after delay ms of inactivity.
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Usage
const log = debounce((msg) => {
console.log(msg, new Date().toISOString());
}, 1000);
log('Hello'); // Reset timer
log('World'); // Reset timer
log('!'); // Only this one fires after 1s of silence
fn. ...args captures all arguments. fn.apply(this, args) preserves the correct this context and passes all arguments.
Flatten a nested array by one level: [1, [2, 3], [4, [5, 6]]] → [1, 2, 3, 4, [5, 6]]
// Method 1: Built-in flat(1)
function flattenOneLevel(arr) {
return arr.flat(1);
}
// Method 2: Using concat + spread (for interviews)
function flattenOneLevel(arr) {
return [].concat(...arr);
}
// Method 3: Manual reduce
function flattenOneLevel(arr) {
return arr.reduce((acc, val) => {
return acc.concat(Array.isArray(val) ? val : [val]);
}, []);
}
console.log(flattenOneLevel([1, [2, 3], [4, [5, 6]]]));
// Output: [1, 2, 3, 4, [5, 6]]
arr.flat(1) is the cleanest approach (ES2019). [].concat(...arr) spreads the array — each element is concatenated, sub-arrays stay intact (one level). The reduce approach is explicit and shows you understand the pattern. Interviewers often ask for manual implementations.
Group an array of objects by a key (e.g., age):
// Input
[{ name: "A", age: 20 }, { name: "B", age: 20 }, { name: "C", age: 21 }]
// Output
{ 20: [{name:"A",age:20}, {name:"B",age:20}], 21: [{name:"C",age:21}] }
function groupByKey(arr, key) {
return arr.reduce((groups, item) => {
const val = item[key];
if (!groups[val]) {
groups[val] = [];
}
groups[val].push(item);
return groups;
}, {});
}
const people = [
{ name: "A", age: 20 },
{ name: "B", age: 20 },
{ name: "C", age: 21 }
];
console.log(groupByKey(people, 'age'));
reduce builds up a single object. For each item, we get the grouping value (item[key]), create an array if it doesn't exist yet, then push the item. The accumulator starts as {}. This is a classic pattern for data transformation.
Remove duplicates from an array of objects based on a key (e.g., "id"), keeping the first occurrence.
function uniqueByKey(arr, key) {
const seen = new Map();
return arr.filter(item => {
if (seen.has(item[key])) return false;
seen.set(item[key], true);
return true;
});
}
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice Duplicate' },
{ id: 3, name: 'Charlie' },
{ id: 2, name: 'Bob Duplicate' }
];
console.log(uniqueByKey(users, 'id'));
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]
Map to track seen keys. filter keeps only the first occurrence of each key. Map.has() is O(1), so this runs in O(n). Alternative: use [...new Map(arr.map(item => [item[key], item])).values()] (more concise but less readable).
Without using fs.promises, write a function that wraps callback-based fs.readFile in a promise.
const fs = require('fs');
function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
// Usage
readFileAsync('input.txt')
.then(content => console.log(content))
.catch(err => console.error('Error:', err.message));
// With async/await
async function main() {
try {
const content = await readFileAsync('input.txt');
console.log(content);
} catch (err) {
console.error('Error:', err.message);
}
}
resolve(data) on success, reject(err) on failure. Node.js has util.promisify that does this automatically: const readFile = util.promisify(fs.readFile).
Larger, more complete exercises combining multiple concepts.
Full CRUD: GET list, POST create, PATCH toggle/update, DELETE. Add validation (title required) and proper status codes.
const express = require('express');
const app = express();
app.use(express.json());
let todos = [];
let nextId = 1;
// GET /todos — list all
app.get('/todos', (req, res) => {
res.json(todos);
});
// POST /todos — create
app.post('/todos', (req, res) => {
const { title, completed } = req.body;
if (!title || typeof title !== 'string') {
return res.status(400).json({ error: 'Title is required and must be a string' });
}
const todo = {
id: nextId++,
title,
completed: completed || false
};
todos.push(todo);
res.status(201).json(todo);
});
// PATCH /todos/:id — toggle or update
app.patch('/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todo = todos.find(t => t.id === id);
if (!todo) return res.status(404).json({ error: 'Todo not found' });
const { title, completed } = req.body;
if (title !== undefined) todo.title = title;
if (completed !== undefined) todo.completed = completed;
else todo.completed = !todo.completed; // toggle if not provided
res.json(todo);
});
// DELETE /todos/:id
app.delete('/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = todos.findIndex(t => t.id === id);
if (index === -1) return res.status(404).json({ error: 'Todo not found' });
todos.splice(index, 1);
res.json({ message: 'Todo deleted' });
});
// Error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000);
completed is in the body, use that value; otherwise flip it. This is a common pattern in todo apps.
POST /upload accepts JSON { filename, content }, writes to uploads/, returns success message. Handle errors.
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.use(express.json());
const UPLOADS_DIR = path.join(__dirname, 'uploads');
// Ensure uploads directory exists
if (!fs.existsSync(UPLOADS_DIR)) {
fs.mkdirSync(UPLOADS_DIR);
}
app.post('/upload', (req, res) => {
const { filename, content } = req.body;
if (!filename || content === undefined) {
return res.status(400).json({ error: 'filename and content are required' });
}
// Basic sanitization — prevent path traversal
const safeName = path.basename(filename);
const filePath = path.join(UPLOADS_DIR, safeName);
fs.writeFile(filePath, content, 'utf8', (err) => {
if (err) {
return res.status(500).json({ error: 'Failed to write file', message: err.message });
}
res.json({ message: 'File saved', path: `uploads/${safeName}` });
});
});
app.listen(3000);
path.basename(filename) strips directory parts (e.g., "../../etc/passwd" becomes "passwd"), preventing path traversal attacks. Always validate and sanitize user-supplied filenames. In production, use a library like multer for multipart form uploads.
Extra questions based on common TCS and Node.js interview patterns.
Given an array of promises (some may reject), use Promise.allSettled to get the result of each — whether fulfilled or rejected — and log a summary.
function delay(ms, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) reject(new Error(`Failed after ${ms}ms`));
else resolve(`Resolved after ${ms}ms`);
}, ms);
});
}
async function main() {
const promises = [
delay(100),
delay(200, true),
delay(300),
delay(150, true),
];
const results = await Promise.allSettled(promises);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${i}: ✅ ${result.value}`);
} else {
console.log(`Promise ${i}: ❌ ${result.reason.message}`);
}
});
}
main();
allSettled never rejects — it waits for all promises and returns an array of { status, value } or { status, reason } objects. Promise.all is fail-fast. Use allSettled when you want to know which promises succeeded and which failed.
Write a function that parses a CSV string into an array of objects using the first row as headers.
// Input: "name,age,city\nAlice,30,NYC\nBob,25,LA"
// Output: [{name:"Alice",age:"30",city:"NYC"}, {name:"Bob",age:"25",city:"LA"}]
function parseCSV(csvString) {
const lines = csvString.trim().split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map(line => {
const values = line.split(',');
const obj = {};
headers.forEach((header, i) => {
obj[header] = values[i];
});
return obj;
});
}
// Usage
const csv = "name,age,city\nAlice,30,NYC\nBob,25,LA";
console.log(parseCSV(csv));
// [{ name: 'Alice', age: '30', city: 'NYC' },
// { name: 'Bob', age: '25', city: 'LA' }]
csv-parse package.
Build a function scheduleTasks(tasks, delay) that runs an array of async functions sequentially, waiting delay ms between each.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function scheduleTasks(tasks, delayMs) {
const results = [];
for (const task of tasks) {
const result = await task();
results.push(result);
if (delayMs > 0) {
await delay(delayMs);
}
}
return results;
}
// Usage
const tasks = [
async () => { console.log('Task 1 done'); return 'result1'; },
async () => { console.log('Task 2 done'); return 'result2'; },
async () => { console.log('Task 3 done'); return 'result3'; },
];
scheduleTasks(tasks, 1000).then(results => {
console.log('All results:', results);
});
for...of loop with await ensures tasks run sequentially. After each task completes, we optionally wait delayMs. Results are collected in order. This pattern is useful for rate-limiting API calls or processing items with delays between them.
Convert a JavaScript object into a URL query string: { name: "John", age: 30 } → "name=John&age=30"
function toQueryString(obj) {
return Object.entries(obj)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
console.log(toQueryString({ name: "John", age: 30, city: "New York" }));
// "name=John&age=30&city=New%20York"
// Alternative using URLSearchParams
function toQueryString2(obj) {
return new URLSearchParams(obj).toString();
}
console.log(toQueryString2({ name: "John", age: 30 }));
// "name=John&age=30"
Object.entries() returns [[key, value], ...]. encodeURIComponent handles special characters (spaces → %20, etc.). URLSearchParams is the built-in browser/Node API that does this automatically. Know both approaches for interviews.
Reverse the order of words in a string: "Hello World from Node" → "Node from World Hello"
function reverseWords(str) {
return str.split(' ').reverse().join(' ');
}
console.log(reverseWords("Hello World from Node"));
// "Node from World Hello"
// Edge case: multiple spaces
function reverseWordsClean(str) {
return str.trim().split(/\s+/).reverse().join(' ');
}
console.log(reverseWordsClean(" Hello World "));
// "World Hello"
split(' ') splits by single space. split(/\s+/) handles multiple spaces. trim() removes leading/trailing whitespace. reverse() reverses the array in place. join(' ') joins with a single space. This 3-method chain is a classic one-liner.
Predict the output order of this code and explain why:
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
process.nextTick(() => {
console.log('4');
});
console.log('5');
// Output order:
// 1
// 5
// 4
// 3
// 2
Cover all topics systematically. Focus on patterns you find difficult.