If you’re preparing for JavaScript interviews or want to improve your coding skills, you’re in the right place. In this blog, we’ve put together some tricky challenges that often show up in technical interviews. You’ll find 21 advanced problems covering data types, variables, switch statements, functions, arrays, and loops. These are the kinds of challenges that test your skills and help you stand out as a developer.
Give them a try and see how many you can solve.
Ready to take on tough JavaScript challenges? Join Index.dev for high-paying remote jobs with top global companies!
Data Types Challenges
Challenge 1: Deep Type Check
Why It’s Tough
Checking the type of a value is simple—until it involves complex objects, arrays, or edge cases like null or NaN.
Sample Task
Write a function deepTypeCheck(value) that returns the exact type of any value. Handle edge cases like null, arrays, and functions.
Solution Example
function deepTypeCheck(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
}
// Examples:
console.log(deepTypeCheck(null)); // 'null'
console.log(deepTypeCheck([1, 2, 3])); // 'array'
console.log(deepTypeCheck(() => {})); // 'function'
console.log(deepTypeCheck(42)); // 'number'Challenge 2: The Symbol Puzzle
Why It's Tough
Symbols are unique identifiers, but they have some surprising behaviors when used as object properties.
Sample Task
Create a function that uses Symbols to make certain object properties truly private.
function createSecureObject(publicData, privateData) {
const privateKey = Symbol('private');
return {
[privateKey]: privateData,
public: publicData,
hasAccess(key) {
return key === privateKey;
},
getPrivateData(key) {
if (!this.hasAccess(key)) {
throw new Error('Access denied!');
}
return this[privateKey];
}
};
}
const obj = createSecureObject('public info', 'secret stuff');
console.log(obj.public); // 'public info'
console.log(Object.keys(obj)); // ['public']
// The private data is not enumerable and not accessible!Challenge 3: Type Coercion
Why It's Tough
Understanding type coercion in JavaScript can be tricky due to its dynamic nature. Developers must recognize how different types interact, especially when using operators.
Sample Task
Write a function that takes two parameters and returns their sum. However, if either parameter is a string containing a number, it should be converted to a number before summing.
Solution Example
function sum(a, b) {
return Number(a) + Number(b);
}
// Example usage:
console.log(sum("5", 10)); // Output: 15
console.log(sum(5, "10")); // Output: 15
Variables Challenges
Challenge 4: Variable Scope
Why It’s Tough
Understanding scope in JavaScript can be challenging, especially with closures and block scope introduced in ES6.
Sample Task
Write a function that returns another function which increments a counter variable. The counter should maintain its state between calls.
Solution Example
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
// Example usage:
const counter = createCounter();
console.log(counter()); // Output: 1
console.log(counter()); // Output: 2Challenge 5: Temporal Dead Zone
Why It’s Tough
Understanding variable hoisting and the temporal dead zone is crucial but complex.
Sample Task
Fix the code to make it work without changing the order of the console.log statements.
Solution Example
function temporalChallenge() {
console.log(a); // Should log 'outer'
console.log(b); // Should log undefined
console.log(c); // Should log ReferenceError
var b = 1;
let c = 2;
{
let a = 'inner';
console.log(a); // Should log 'inner'
}
}
// Solution:
function temporalChallenge() {
let a = 'outer'; // Moved here
console.log(a);
console.log(b);
try {
console.log(c);
} catch(e) {
console.log('ReferenceError');
}
var b = 1;
let c = 2;
{
let a = 'inner';
console.log(a);
}
}Challenge 6: Closures
Why It’s Tough
This tests your understanding of variable scope and closure behavior in loops.
Sample Task
Create a series of functions that each return their index in the series, without using let or additional closure scope.
Solution Example
function createFunctions(n) {
var functions = [];
// This common approach doesn't work:
for (var i = 0; i < n; i++) {
functions.push(function() { return i; });
}
// Solution using bind:
for (var i = 0; i < n; i++) {
functions.push(function(x) {
return x;
}.bind(null, i));
}
return functions;
}
const fns = createFunctions(3);
console.log(fns[0]()); // 0
console.log(fns[1]()); // 1
console.log(fns[2]()); // 2Explore More: How to Send Data as Variables with JavaScript POST Requests
Switch Statements Challenges
Challenge 7: Implement Range Matching in a Switch
Why It’s Tough
Switch statements don’t naturally support ranges, so you have to get creative.
Sample Task
Use a switch to categorize a number (score) into low, medium, or high.
Solution Example
function categorizeScore(score) {
switch (true) {
case score < 50:
return 'low';
case score < 80:
return 'medium';
case score >= 80:
return 'high';
default:
return 'unknown';
}
}
// Example:
console.log(categorizeScore(45)); // 'low'
console.log(categorizeScore(75)); // 'medium'
console.log(categorizeScore(85)); // 'high'Challenge 8: The Fallthrough
Why It's Tough
This tests your knowledge of switch statement fall-through behavior and scope.
Sample Task
Write a switch statement that uses fallthrough behavior intentionally to implement a number-to-text converter.
Solution Example
function numberToText(num) {
let result = '';
switch (true) {
case (num >= 1000):
result += 'thousand ';
num %= 1000;
// Intentional fallthrough!
case (num >= 100):
result += Math.floor(num / 100) + ' hundred ';
num %= 100;
// Intentional fallthrough!
case (num >= 10):
result += ['ten', 'twenty', 'thirty', 'forty'][Math.floor(num / 10) - 1] + ' ';
num %= 10;
// Intentional fallthrough!
case (num >= 0):
if (num > 0) {
result += ['one', 'two', 'three'][num - 1];
}
}
return result.trim();
}
console.log(numberToText(123)); // "one hundred twenty three"Challenge 9: Dynamic Switch Cases
Why It’s Tough
Most developers don't know you can use expressions in case statements.
Sample Task
Implement a calculator that uses dynamic switch cases to handle different operations.
Solution Example
function calculate(a, b, operation) {
switch (true) {
case (operation === '+' ||
operation.toLowerCase() === 'add'):
return a + b;
case (operation === '*' &&
typeof a === 'number' &&
typeof b === 'number'):
return a * b;
case (/^div/i.test(operation)):
if (b === 0) throw new Error('Division by zero');
return a / b;
default:
throw new Error('Unknown operation');
}
}
console.log(calculate(5, 3, 'Add')); // 8
console.log(calculate(5, 3, '*')); // 15
console.log(calculate(15, 3, 'divide')); // 5
Functions Challenges
Challenge 10: Recursive Function for Nested Data
Why It’s Tough
Recursion can be tricky, especially with nested data and edge cases like empty or circular references.
Sample Task
Write a recursive function sumNestedNumbers(obj) that calculates the sum of all numbers in a deeply nested object.
Solution Example
function sumNestedNumbers(obj) {
let sum = 0;
for (let key in obj) {
if (typeof obj[key] === 'number') {
sum += obj[key];
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
sum += sumNestedNumbers(obj[key]);
}
}
return sum;
}
// Example:
const data = { a: 1, b: { c: 2, d: { e: 3 } }, f: 4 };
console.log(sumNestedNumbers(data)); // Output: 10Challenge 11: Function Memoization
Why It’s Tough
Optimizing functions with memoization requires understanding closures and performance optimization techniques.
Sample Task
Write a memoized version of a function fibonacci(n) that calculates the nth Fibonacci number.
Solution Example
function memoize(fn) {
const cache = {};
return function (...args) {
const key = args.toString();
if (cache[key]) return cache[key];
const result = fn(...args);
cache[key] = result;
return result;
};
}
const fibonacci = memoize((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
// Example:
console.log(fibonacci(10)); // Output: 55
console.log(fibonacci(50)); // Output: 12586269025 (efficient)Challenge 12: The Currying Constructor
Why It's Tough
This combines function currying with constructor patterns.
Sample Task
Create a curried function that can also be used as a constructor.
Solution Example
function CurriedConstructor(a) {
if (!(this instanceof CurriedConstructor)) {
return function(b) {
return function(c) {
return new CurriedConstructor(a + b + c);
};
};
}
this.value = a;
}
// Use as a constructor
const instance = new CurriedConstructor(1);
console.log(instance.value); // 1
// Use as a curried function
const result = CurriedConstructor(1)(2)(3);
console.log(result.value); // 6
Arrays Challenges
Challenge 13: Find Duplicates in an Array
Why It’s Tough
Identifying duplicates efficiently requires knowledge of data structures like sets or maps.
Sample Task
Create a function that returns an array of duplicate values from an input array.
Solution Example
function findDuplicates(arr) {
const seen = new Set();
const duplicates = new Set();
arr.forEach(item => {
if (seen.has(item)) {
duplicates.add(item);
} else {
seen.add(item);
}
});
return [...duplicates];
}
// Example usage:
console.log(findDuplicates([1, 2, 3, 4, 3, 2])); // Output: [2, 3]Challenge 14: Custom Array Flattening
Why It’s Tough
Deep flattening arrays with custom logic is harder than it looks.
Sample Task
Write a function that flattens an array to a specific depth while filtering values.
Solution Example
function customFlatten(arr, depth = 1, predicate = () => true) {
return arr.reduce((flat, item) => {
if (Array.isArray(item) && depth > 0) {
return flat.concat(
customFlatten(item, depth - 1, predicate)
);
}
return predicate(item) ? flat.concat(item) : flat;
}, []);
}
const nested = [1, [2, 3, [4, 5]], 6, [7, 8]];
const isEven = x => x % 2 === 0;
console.log(customFlatten(nested, 1));
// [1, 2, 3, [4, 5], 6, 7, 8]
console.log(customFlatten(nested, Infinity, isEven));
// [2, 4, 6, 8]Challenge 15: Array Group By With Reducers
Why It’s Tough
This combines array methods with custom reducer functions.
Sample Task
Implement a groupBy function that can handle custom reducers for each group.
Solution Example
function groupByWithReducers(arr, keyFn, reducers) {
return arr.reduce((groups, item) => {
const key = keyFn(item);
if (!groups[key]) {
groups[key] = Object.keys(reducers).reduce((acc, reducerKey) => {
acc[reducerKey] = reducers[reducerKey].initial();
return acc;
}, {});
}
Object.entries(reducers).forEach(([reducerKey, reducer]) => {
groups[key][reducerKey] = reducer.reduce(
groups[key][reducerKey],
item
);
});
return groups;
}, {});
}
const data = [
{ category: 'A', value: 1 },
{ category: 'B', value: 2 },
{ category: 'A', value: 3 }
];
const reducers = {
sum: {
initial: () => 0,
reduce: (acc, item) => acc + item.value
},
count: {
initial: () => 0,
reduce: (acc) => acc + 1
}
};
console.log(groupByWithReducers(
data,
item => item.category,
reducers
));
// {
// A: { sum: 4, count: 2 },
// B: { sum: 2, count: 1 }
// }Also Read: 6 Easy Ways To Convert String to Date in JavaScript
Loops Challenges
Challenge 16: Async Iterator Pattern
Why It’s Tough
This combines async/await with iterator patterns.
Sample Task
Create an async iterator that yields values with a delay and can be used in a for-await-of loop.
Solution Example
function createAsyncIterator(array, delay = 1000) {
return {
[Symbol.asyncIterator]() {
let index = 0;
return {
async next() {
if (index >= array.length) {
return { done: true };
}
await new Promise(resolve =>
setTimeout(resolve, delay)
);
return {
value: array[index++],
done: false
};
}
};
}
};
}
async function processWithDelay() {
const iterator = createAsyncIterator([1, 2, 3]);
for await (const value of iterator) {
console.log(value); // Logs each value with delay
}
}Challenge 17: Generator-based Event Loop
Why It’s Tough
This tests understanding of generators and event handling.
Sample Task
Implement a simple event loop using generators that can handle both sync and async tasks.
Solution Example
function* eventLoop() {
const tasks = new Set();
let currentTask;
while (true) {
const { type, task, resolve } = yield;
if (type === 'add') {
tasks.add({ task, resolve });
}
while (currentTask = tasks.values().next().value) {
tasks.delete(currentTask);
try {
const result = currentTask.task();
if (result && typeof result.then === 'function') {
result.then(value => {
currentTask.resolve(value);
});
} else {
currentTask.resolve(result);
}
} catch (error) {
currentTask.resolve(Promise.reject(error));
}
}
}
}
const loop = eventLoop();
loop.next(); // Start the loop
function addTask(task) {
return new Promise(resolve => {
loop.next({
type: 'add',
task,
resolve
});
});
}
// Example usage:
addTask(() => 'sync task').then(console.log);
addTask(() => Promise.resolve('async task')).then(console.log);Challenge 18: FizzBuzz Variation
Why It’s Tough
Implementing variations of FizzBuzz tests logical thinking and control flow understanding.
Sample Task
Write a loop that prints numbers from 1 to N. For multiples of three print "Fizz", for multiples of five print "Buzz", and for multiples of both print "FizzBuzz".
Solution Example
function fizzBuzz(n) {
for (let i = 1; i <= n; i++) {
let output = '';
if (i % 3 === 0) output += 'Fizz';
if (i % 5 === 0) output += 'Buzz';
console.log(output || i);
}
}
// Example usage:
fizzBuzz(15);
// Outputs numbers from 1 to 15 with Fizz/Buzz/FizzBuzz as appropriate.
Practical Challenges to Test Your Abilities
Below you will find three challenges that test practical problem-solving, algorithmic thinking, and the ability to write efficient and clear JavaScript code. Each task simulates a real-world problem.
Challenge 19: Build a Simple Event Emitter
Why It’s Tough
Event-driven programming is foundational in JavaScript, especially in frameworks like Node.js and React. Implementing an event emitter tests your ability to manage subscriptions and event listeners.
Sample Task
Create a class EventEmitter with methods on(event, listener), emit(event, ...args), and off(event, listener).
Solution Example
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(listener);
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(...args));
}
}
off(event, listener) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(l => l !== listener);
}
}
}
// Example usage:
const emitter = new EventEmitter();
const greet = (name) => console.log(`Hello, ${name}!`);
emitter.on('greet', greet);
emitter.emit('greet', 'Alice'); // Output: Hello, Alice!
emitter.off('greet', greet);
emitter.emit('greet', 'Alice'); // No outputWhat It Tests
- Object-oriented programming concepts.
- Managing collections of functions (event listeners).
- Understanding core patterns in JavaScript frameworks.
Challenge 20: Build a Debounced API Search
Real-World Scenario
You're building a search feature that needs to query an API as the user types, but you want to minimize API calls.
Sample Task
Create a search function that:
- Only makes an API call after the user stops typing for 300ms
- Cancels pending requests if a new search starts
- Shows a loading state while fetching
- Handles errors gracefully
Solution Example
class SearchWidget {
constructor() {
this.lastTimeout = null;
this.currentRequest = null;
this.isLoading = false;
}
async search(query) {
// Clear any pending timeouts
if (this.lastTimeout) {
clearTimeout(this.lastTimeout);
}
// Cancel any ongoing requests
if (this.currentRequest) {
this.currentRequest.abort();
}
// Create new AbortController for this request
const controller = new AbortController();
this.currentRequest = controller;
return new Promise((resolve, reject) => {
this.lastTimeout = setTimeout(async () => {
try {
this.isLoading = true;
this.updateUI({ loading: true });
const response = await fetch(
`https://api.example.com/search?q=${encodeURIComponent(query)}`,
{
signal: controller.signal
}
);
if (!response.ok) {
throw new Error('Search failed');
}
const data = await response.json();
this.updateUI({
loading: false,
results: data
});
resolve(data);
} catch (error) {
if (error.name === 'AbortError') {
// Request was cancelled, ignore
return;
}
this.updateUI({
loading: false,
error: error.message
});
reject(error);
} finally {
this.isLoading = false;
}
}, 300);
});
}
updateUI(state) {
// Example UI updates
const searchResults = document.querySelector('.search-results');
const loadingSpinner = document.querySelector('.loading-spinner');
loadingSpinner.style.display = state.loading ? 'block' : 'none';
if (state.error) {
searchResults.innerHTML = `<div class="error">${state.error}</div>`;
} else if (state.results) {
searchResults.innerHTML = state.results
.map(result => `<div class="result">${result.title}</div>`)
.join('');
}
}
}
// Usage:
const search = new SearchWidget();
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', (e) => {
search.search(e.target.value);
});What It Tests
- Debouncing implementation
- Async/Await & promise handling
- API request management
- UI updates and DOM manipulation
- Clean code & best practices
Challenge 21: Implement a Virtual Scroll
Real-World Scenario
You need to display a list of thousands of items without impacting performance.
Sample Task
Create a virtual scroll component that:
- Only renders items currently in view
- Smoothly handles scroll events
- Maintains scroll position when items change
- Supports variable height items
Solution Example
class VirtualScroll {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleItems = Math.ceil(container.clientHeight / itemHeight) + 2;
this.scrollTop = 0;
this.startIndex = 0;
this.init();
}
init() {
// Create scrollable content
this.content = document.createElement('div');
this.content.style.height = `${this.items.length * this.itemHeight}px`;
this.content.style.position = 'relative';
// Create viewport
this.viewport = document.createElement('div');
this.viewport.style.height = '100%';
this.viewport.style.overflow = 'auto';
this.viewport.appendChild(this.content);
this.container.appendChild(this.viewport);
this.viewport.addEventListener('scroll', this.onScroll.bind(this));
this.render();
}
onScroll(event) {
this.scrollTop = event.target.scrollTop;
this.render();
}
render() {
// Calculate visible range
this.startIndex = Math.floor(this.scrollTop / this.itemHeight);
const endIndex = Math.min(
this.startIndex + this.visibleItems,
this.items.length
);
// Clear current content
this.content.innerHTML = '';
// Render only visible items
for (let i = this.startIndex; i < endIndex; i++) {
const item = document.createElement('div');
item.style.position = 'absolute';
item.style.top = `${i * this.itemHeight}px`;
item.style.height = `${this.itemHeight}px`;
item.innerHTML = this.items[i];
this.content.appendChild(item);
}
}
// Handle window resize
resize() {
this.visibleItems = Math.ceil(this.container.clientHeight / this.itemHeight) + 2;
this.render();
}
// Update items
updateItems(newItems) {
this.items = newItems;
this.content.style.height = `${this.items.length * this.itemHeight}px`;
this.render();
}
}
// Usage:
const container = document.querySelector('#list-container');
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
const virtualScroll = new VirtualScroll(container, items, 50);
// Handle window resize
window.addEventListener('resize', () => virtualScroll.resize());What It Tests
- DOM performance optimization
- Scroll event handling
- State management
- Memory management
Rewrite and improve the following text, and add what’s missing. Keep it clear, simple and easy to read. Use simple language. Simple words and short sentences. Write like a human. Conversational, friendly, informative tone.
Read Also: 5 Ways to Store Data within a HTML File Using JavaScript
Tips to Prepare for JavaScript Coding Challenges
Let’s face it—coding challenges can be nerve-wracking. They’re not just about solving the problem; they’re about showing how you think, code efficiently, and handle tricky scenarios. With the right preparation, you’ll be ready to tackle them confidently.
Here’s how:
- Revisit key concepts like loops, control flow, and data types using online resources like freeCodeCamp or MDN.
- Solve one challenge every day on platforms like LeetCode, HackerRank, or CodeSignal.
- Don’t memorize solutions—break problems into steps, understand the logic, and think aloud as you solve.
- Practice coding on a whiteboard or plain text editor. Time yourself and do mock interviews to build confidence.
- Learn modern JavaScript features like async/await, destructuring, and spread operators.
- Study common patterns like sliding windows, recursion, or two-pointers—they’re used in many problems.
- Join forums like Stack Overflow, Reddit, or coding contests to learn from others and improve under pressure.
Conclusion
That's it! These challenges cover some of the trickiest aspects of JavaScript that you might encounter in interviews. Remember, the key to solving these isn't just knowing the syntax, but understanding the underlying concepts and patterns.
Keep practicing, and don't get discouraged if you don't get them right away. Every developer has struggled with these concepts at some point!
Till then, happy coding🚀
For JavaScript Developers:
Take your JavaScript skills to the next level and work on exciting, high-paying remote projects with global companies. Join Index.dev today!
For Clients:
Hire skilled JavaScript developers for your next project with Index.dev’s global talent network. Access vetted talent ready for long-term, high-quality work.