For DevelopersJanuary 02, 2025

21 Advanced JavaScript Challenges That Test Your Skills

Explore 21 advanced JavaScript coding challenges that will prepare you for tough technical interviews.

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: 2

Challenge 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]());  // 2

Explore 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: 10

Challenge 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 output

What 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:

  1. Revisit key concepts like loops, control flow, and data types using online resources like freeCodeCamp or MDN.
  2. Solve one challenge every day on platforms like LeetCode, HackerRank, or CodeSignal.
  3. Don’t memorize solutions—break problems into steps, understand the logic, and think aloud as you solve.
  4. Practice coding on a whiteboard or plain text editor. Time yourself and do mock interviews to build confidence.
  5. Learn modern JavaScript features like async/await, destructuring, and spread operators.
  6. Study common patterns like sliding windows, recursion, or two-pointers—they’re used in many problems.
  7. 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.

Share

Radu PoclitariRadu PoclitariCopywriter

Related Articles

For EmployersWhy High Retention of Engineering Talent is the Hidden Advantage Your Projects Need
Tech HiringInsights
High engineer retention is a direct multiplier on your team's speed, output, and cost efficiency. Replacing a developer can cost up to 150% of their salary, and that's before counting the lost context, the delayed sprints, and the months of ramp-up time. Here's what retention actually does to your bottom line, and how Index.dev makes it the default.
Mihai GolovatencoMihai GolovatencoTalent Director
NewsInside Index.dev's Latest NPS: What 8 Surveys in a Row Are Teaching Us
Index.dev has run its Net Promoter Score survey for eight consecutive periods, keeping NPS above 70 every time. Engineers consistently praise collaboration, autonomy, reliable payments, and support. This blog reveals what keeps our network engaged and how we act on their insights.
Elena BejanElena BejanPeople Culture and Development Director