Karat-Style Technical Interviews: A Complete JavaScript Practice Guide with Solutions
Technical interviews can feel difficult even for experienced developers. The challenge is not only finding the correct solution. You must also understand the requirements, explain your thinking, select suitable data structures, write working code, test it, and discuss its performance—all while an interviewer observes your process.
Karat-style interviews are designed to evaluate this complete problem-solving process. According to the official Karat Candidate Experience, a standard interview generally lasts around one hour and contains three main sections:
A brief introduction
Approximately 10 minutes of technical discussion
Approximately 40 minutes of programming questions
The exact format can vary depending on the hiring company and position. Some companies may begin with a shorter 30-minute coding assessment before inviting candidates to a complete live interview.
This guide explains the most important Karat-style question patterns and provides original JavaScript practice problems with complete solutions. These examples are not leaked or official Karat questions. They are designed to help you practise the same skills and interview structure.
The interviewer is not evaluating only the final code. They are also observing how you work through the problem.
They usually look for the following skills:
Understanding and clarifying requirements
Breaking a problem into smaller steps
Choosing appropriate data structures
Explaining technical decisions
Writing clean and understandable code
Identifying edge cases
Testing and debugging
Analysing time and space complexity
Responding to changing requirements
A candidate may produce an incomplete solution but still demonstrate strong engineering skills. On the other hand, silently writing a memorized solution may not provide enough evidence of genuine problem-solving ability.
Before studying individual problems, you should learn a consistent answering process.
Do not begin coding immediately. First, repeat the requirement in your own words.
You can say:
Let me confirm my understanding. The function receives a list of activity records, and I need to return the users who satisfy all the required conditions.
This gives the interviewer an opportunity to correct any misunderstanding before you spend time implementing the wrong solution.
Useful questions include:
Can the input be empty?
Can it contain duplicate values?
Is the input already sorted?
Can I modify the original input?
What should I return if no result exists?
Does the result order matter?
How large can the input be?
Are all values guaranteed to be valid?
Do not ask questions only to impress the interviewer. Ask questions that could genuinely change your implementation.
Before coding, explain the main idea.
For example:
A simple solution would compare every item with every other item, but that would take O(n²) time. We can improve it to O(n) by storing previously seen values in a Map.
This demonstrates that you understand both the simple solution and the optimized solution.
Write the solution in small, understandable steps. Use descriptive variable names and explain important decisions while coding.
Avoid staying silent for several minutes.
Test at least:
The provided example
Empty or minimal input
A boundary case
Duplicate values
A case where no result exists
Always state both time and space complexity.
For example:
We visit every record once, so the time complexity is O(n). The Map may store up to n entries, so the space complexity is also O(n).
Question Style 1: Arrays, Maps, and Sets
Many interview questions require grouping records, counting values, removing duplicates, or checking whether something was previously seen.
JavaScript’s Map and Set are especially useful for these problems.
You receive a list of course-completion records:
const completions = [
["Alaa", "JavaScript"],
["Sara", "JavaScript"],
["Alaa", "React"],
["Omar", "JavaScript"],
["Alaa", "Laravel"],
["Sara", "React"],
["Sara", "Laravel"],
["Sara", "React"]
];
const requiredCourses = [
"JavaScript",
"React",
"Laravel"
];
Return the users who completed every required course.
Expected result:
["Alaa", "Sara"]
Notice that Sara’s React completion appears twice. The duplicate should not affect the result.
Create a Map where:
The key is the user’s name.
The value is a Set of required courses completed by that user.
A Set is useful because it automatically ignores duplicate values.
function usersCompletingAllCourses(
completions,
requiredCourses
) {
const required = new Set(requiredCourses);
const coursesByUser = new Map();
for (const [user, course] of completions) {
if (!coursesByUser.has(user)) {
coursesByUser.set(user, new Set());
}
if (required.has(course)) {
coursesByUser.get(user).add(course);
}
}
const result = [];
for (const [user, completedCourses] of coursesByUser) {
if (completedCourses.size === required.size) {
result.push(user);
}
}
return result;
}
If there are c completion records and r required courses:
Time complexity: O(c + r)
Space complexity: O(c + r)
The interviewer might ask:
What if each user must complete every course at least twice?
In that case, a Set would no longer be sufficient because it does not store counts. You would use a nested Map:
Map<user, Map<course, count>>
This is a typical Karat-style progression: the first requirement tests basic grouping, while the follow-up requires changing the data structure.
Question Style 2: Timestamps and Sliding Windows
Time-based problems often require:
Converting timestamps
Sorting records
Finding events inside a time window
Handling inclusive and exclusive boundaries
Given employee badge records in HHMM format, return employees who used their badges at least three times within any 60-minute period.
const records = [
["Paul", "1315"],
["Jennifer", "1910"],
["Paul", "1355"],
["Paul", "1405"],
["Jennifer", "2005"],
["Jennifer", "2100"]
];
Expected result:
{
Paul: ["1315", "1355", "1405"]
}
Assume that exactly 60 minutes is included.
The solution has four main steps:
Group timestamps by employee.
Convert each timestamp into minutes after midnight.
Sort each employee’s timestamps.
Use a sliding window to find three or more records within 60 minutes.
function toMinutes(time) {
const hours = Number(time.slice(0, 2));
const minutes = Number(time.slice(2));
return hours * 60 + minutes;
}
function findFrequentBadgeUse(records) {
const timesByEmployee = new Map();
for (const [employee, time] of records) {
if (!timesByEmployee.has(employee)) {
timesByEmployee.set(employee, []);
}
timesByEmployee.get(employee).push({
original: time,
minutes: toMinutes(time)
});
}
const result = {};
for (const [employee, times] of timesByEmployee) {
times.sort((a, b) => a.minutes - b.minutes);
let left = 0;
for (let right = 0; right < times.length; right++) {
while (
times[right].minutes -
times[left].minutes >
60
) {
left++;
}
const windowSize = right - left + 1;
if (windowSize >= 3) {
result[employee] = times
.slice(left, right + 1)
.map(entry => entry.original);
break;
}
}
}
return result;
}
The left and right variables represent the current time window.
When the difference between the newest and oldest event becomes greater than 60 minutes, we move left forward until the window is valid again.
This avoids comparing every timestamp with every other timestamp.
For n records:
Time complexity: O(n log n), primarily because of sorting
Space complexity: O(n)
Be careful with the boundary condition.
This requirement includes exactly 60 minutes, so the window becomes invalid only when:
difference > 60
Using difference >= 60 would incorrectly reject records exactly 60 minutes apart.
The interviewer may ask you to:
Support records crossing midnight
Return every qualifying window
Process incoming records as a stream
Return only the earliest qualifying window
Handle invalid timestamps
Question Style 3: Two-Dimensional Grids
Grid questions test:
Nested loops
Row and column indexes
Boundary conditions
Breadth-first search
Depth-first search
A two-dimensional grid represents a warehouse floor:
1 means the position is available.
0 means the position is blocked.
All zeros form exactly one solid rectangle.
Return the rectangle’s top row, left column, width, and height.
const grid = [
[1, 1, 1, 1, 1, 1],
[1, 1, 0, 0, 0, 1],
[1, 1, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1]
];
Expected result:
{
topRow: 1,
leftColumn: 2,
width: 3,
height: 2
}
Because the zeros form one solid rectangle:
Scan the grid until you find the first zero.
The first zero is the rectangle’s top-left corner.
Move right to calculate the width.
Move down to calculate the height.
function findBlockedArea(grid) {
if (
grid.length === 0 ||
grid[0].length === 0
) {
return null;
}
const numberOfRows = grid.length;
const numberOfColumns = grid[0].length;
let topRow = -1;
let leftColumn = -1;
for (let row = 0; row < numberOfRows; row++) {
for (
let column = 0;
column < numberOfColumns;
column++
) {
if (grid[row][column] === 0) {
topRow = row;
leftColumn = column;
break;
}
}
if (topRow !== -1) {
break;
}
}
if (topRow === -1) {
return null;
}
let width = 0;
while (
leftColumn + width < numberOfColumns &&
grid[topRow][leftColumn + width] === 0
) {
width++;
}
let height = 0;
while (
topRow + height < numberOfRows &&
grid[topRow + height][leftColumn] === 0
) {
height++;
}
return {
topRow,
leftColumn,
width,
height
};
}
Starting from the top-left corner, we move horizontally:
grid[topRow][leftColumn + width]
For this row:
0 0 0
The width is three.
Starting from the top-left corner, we move vertically:
grid[topRow + height][leftColumn]
For this column:
0
0
The height is two.
Time complexity: O(rows × columns)
Space complexity: O(1)
The interviewer may change the requirement:
The grid can contain multiple blocked areas, and they are not necessarily rectangles.
This requires BFS or DFS to find connected components.
function findBlockedComponents(grid) {
const rows = grid.length;
const columns = grid[0].length;
const visited = Array.from(
{ length: rows },
() => Array(columns).fill(false)
);
const directions = [
[1, 0],
[-1, 0],
[0, 1],
[0, -1]
];
const components = [];
for (let startRow = 0; startRow < rows; startRow++) {
for (
let startColumn = 0;
startColumn < columns;
startColumn++
) {
const isBlocked =
grid[startRow][startColumn] === 0;
if (
!isBlocked ||
visited[startRow][startColumn]
) {
continue;
}
const queue = [[startRow, startColumn]];
const cells = [];
visited[startRow][startColumn] = true;
let queueIndex = 0;
while (queueIndex < queue.length) {
const [row, column] =
queue[queueIndex++];
cells.push([row, column]);
for (
const [rowChange, columnChange]
of directions
) {
const nextRow = row + rowChange;
const nextColumn =
column + columnChange;
const isInside =
nextRow >= 0 &&
nextRow < rows &&
nextColumn >= 0 &&
nextColumn < columns;
if (
isInside &&
grid[nextRow][nextColumn] === 0 &&
!visited[nextRow][nextColumn]
) {
visited[nextRow][nextColumn] =
true;
queue.push([
nextRow,
nextColumn
]);
}
}
}
components.push(cells);
}
}
return components;
}
The complexity remains:
Time complexity: O(rows × columns)
Space complexity: O(rows × columns)
Question Style 4: Graphs and Relationships
Graph problems frequently appear as real-world relationships rather than mathematical graphs.
Examples include:
Service dependencies
Social connections
Course prerequisites
Folder hierarchies
Employee management structures
Package dependencies
Each pair contains:
[service, dependency]
For example:
const dependencies = [
["Checkout", "Payments"],
["Orders", "Database"],
["Checkout", "Orders"],
["Reports", "Database"],
["Dashboard", "Reports"]
];
If the Database service fails, return every service affected directly or indirectly.
Expected result:
[
"Orders",
"Reports",
"Checkout",
"Dashboard"
]
The result order does not matter.
The input tells us what each service depends on:
Orders depends on Database
However, we need to travel in the opposite direction:
Database affects Orders
Therefore, we create a reverse adjacency list:
Database → Orders, Reports
Orders → Checkout
Reports → Dashboard
We can then use BFS to visit all affected services.
function findAffectedServices(
dependencies,
failedService
) {
const dependents = new Map();
for (const [service, dependency] of dependencies) {
if (!dependents.has(dependency)) {
dependents.set(dependency, []);
}
dependents.get(dependency).push(service);
}
const affected = [];
const visited = new Set([failedService]);
const queue = [failedService];
let queueIndex = 0;
while (queueIndex < queue.length) {
const current = queue[queueIndex++];
const directlyAffected =
dependents.get(current) ?? [];
for (const service of directlyAffected) {
if (visited.has(service)) {
continue;
}
visited.add(seNo approved comments are visible yet. New community replies may wait for moderation.