
Prepare for a Karat technical interview with a realistic 60-minute mock exam, five discussion questions, ten original JavaScript coding problems, detailed solutions, edge cases, follow-ups, and complexity analysis.
A Karat technical interview tests more than code. You must understand requirements, explain decisions, handle edge cases, test your work, and discuss complexity while an interviewer observes your process.
This mock exam contains five discussion questions and ten original JavaScript coding problems. They are practice exercises, not leaked, recalled, or official Karat questions. Content varies by company, role, and seniority.
The official Karat Candidate Experience describes a typical 60-minute interview with a brief introduction, about 10 minutes of discussion questions, and about 40 minutes of programming. Always follow the instructions sent for your interview.
| Time | Activity |
|---|---|
| 0–5 minutes | Give a concise introduction |
| 5–15 minutes | Answer two discussion questions |
| 15–35 minutes | Solve coding problem one |
| 35–55 minutes | Solve coding problem two and a follow-up |
| 55–60 minutes | Test, fix, and summarize complexity |
For every coding question:
Map Instead of an Object?Use an object for a record with known property names. Use Map when keys are dynamic, may be non-strings, or require frequent insertion and lookup.
“For grouping requests by an arbitrary user ID, I would use
Map. For a configuration with fixed fields such ashostandport, I would use an object.”
Map also provides size, has, get, set, and insertion-order iteration.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
Output:
A
D
C
B
Synchronous code runs first. Promise callbacks enter the microtask queue, while the timer callback enters the task queue. Microtasks run before the next task after the current stack is empty.
Explain the product effect, not only the definition.
Completion order is not guaranteed. Depending on the system, use AbortController, request-version checks, idempotency keys, transactions, optimistic locking, or queues.
let latestRequest = 0;
async function search(query) {
const id = ++latestRequest;
const data = await fetchResults(query);
if (id === latestRequest) renderResults(data);
}
This ignores a stale response when a newer search has already started.
Define “slow” using p50, p95, and p99 latency. Determine whether the issue affects one endpoint, customer, or time period. Trace application, database, cache, and external-service time. Check recent changes and resource saturation. Optimize the measured bottleneck, verify the result, and add monitoring.
Return one record per ID. Keep the greatest timestamp while preserving the order in which each ID first appeared.
function latestEvents(events) {
const latest = new Map();
for (const event of events) {
const current = latest.get(event.id);
if (!current || event.timestamp > current.timestamp) {
latest.set(event.id, event);
}
}
return [...latest.values()];
}
latestEvents([
{ id: "A", timestamp: 10, value: "old" },
{ id: "B", timestamp: 20, value: "open" },
{ id: "A", timestamp: 30, value: "new" }
]);
// A at 30, then B at 20
Replacing a map value does not change key insertion order.
k distinct IDsReturn the first ID appearing exactly once, or null.
function firstUnique(ids) {
const counts = new Map();
for (const id of ids) {
counts.set(id, (counts.get(id) ?? 0) + 1);
}
for (const id of ids) {
if (counts.get(id) === 1) return id;
}
return null;
}
firstUnique(["r1", "r2", "r1", "r3", "r2"]); // "r3"
The first pass counts; the second preserves order.
Merge intervals that overlap or touch without modifying the input.
function mergeWindows(windows) {
if (windows.length === 0) return [];
const sorted = windows
.map(([start, end]) => [start, end])
.sort((a, b) => a[0] - b[0]);
const result = [sorted[0]];
for (let i = 1; i < sorted.length; i++) {
const current = sorted[i];
const last = result[result.length - 1];
if (current[0] <= last[1]) {
last[1] = Math.max(last[1], current[1]);
} else {
result.push(current);
}
}
return result;
}
mergeWindows([[30, 60], [10, 20], [20, 25], [55, 80]]);
// [[10, 25], [30, 80]]
Apply changes in order. Reject a change that would make inventory negative.
function updateInventory(initial, changes) {
const inventory = { ...initial };
const rejected = [];
for (const change of changes) {
const current = inventory[change.product] ?? 0;
const next = current + change.amount;
if (next < 0) {
rejected.push(change);
} else {
inventory[change.product] = next;
}
}
return { inventory, rejected };
}
updateInventory(
{ keyboard: 4, mouse: 2 },
[
{ product: "keyboard", amount: -2 },
{ product: "mouse", amount: -3 },
{ product: "monitor", amount: 5 }
]
);
Check (), [], and {} while ignoring other characters.
function isBalanced(source) {
const expected = { ")": "(", "]": "[", "}": "{" };
const opening = new Set(["(", "[", "{"]);
const stack = [];
for (const character of source) {
if (opening.has(character)) {
stack.push(character);
} else if (character in expected) {
if (stack.pop() !== expected[character]) return false;
}
}
return stack.length === 0;
}
isBalanced("{ value: [1, (2 + 3)] }"); // true
isBalanced("([)]"); // false
Allow at most three accepted requests per user in an inclusive 10-second window. Timestamps arrive in order.
function createLimiter(limit, seconds) {
const state = new Map();
return function allow(user, time) {
const data = state.get(user) ?? { times: [], head: 0 };
const minimum = time - seconds;
while (
data.head < data.times.length &&
data.times[data.head] < minimum
) {
data.head++;
}
if (data.times.length - data.head >= limit) {
state.set(user, data);
return false;
}
data.times.push(time);
state.set(user, data);
return true;
};
}
const allow = createLimiter(3, 10);
allow("A", 1); // true
allow("A", 4); // true
allow("A", 8); // true
allow("A", 9); // false
allow("A", 12); // true
Each accepted timestamp enters and leaves the active window once.
[project, dependency] means the dependency must be built first. Return null for a cycle.
function buildOrder(projects, dependencies) {
const next = new Map(projects.map(project => [project, []]));
const degree = new Map(projects.map(project => [project, 0]));
for (const [project, dependency] of dependencies) {
next.get(dependency).push(project);
degree.set(project, degree.get(project) + 1);
}
const queue = projects.filter(project => degree.get(project) === 0);
const result = [];
let head = 0;
while (head < queue.length) {
const completed = queue[head++];
result.push(completed);
for (const project of next.get(completed)) {
degree.set(project, degree.get(project) - 1);
if (degree.get(project) === 0) queue.push(project);
}
}
return result.length === projects.length ? result : null;
}
buildOrder(
["api", "web", "db", "auth"],
[["api", "db"], ["api", "auth"], ["web", "api"]]
);
This is Kahn’s topological sort.
0 is open and 1 is blocked. Move vertically or horizontally.
function shortestPath(grid, start, end) {
const rows = grid.length;
if (rows === 0) return -1;
const queue = [[...start, 0]];
const seen = new Set([start.join(",")]);
const moves = [[1, 0], [-1, 0], [0, 1], [0, -1]];
let head = 0;
while (head < queue.length) {
const [row, column, distance] = queue[head++];
if (row === end[0] && column === end[1]) return distance;
for (const [dr, dc] of moves) {
const nr = row + dr;
const nc = column + dc;
const key = `${nr},${nc}`;
if (
nr >= 0 && nr < rows &&
nc >= 0 && nc < grid[0].length &&
grid[nr][nc] === 0 &&
!seen.has(key)
) {
seen.add(key);
queue.push([nr, nc, distance + 1]);
}
}
}
return -1;
}
Breadth-first search gives the shortest path in an unweighted grid.
Return the k most frequent endpoints. Break ties alphabetically.
function topEndpoints(endpoints, k) {
const counts = new Map();
for (const endpoint of endpoints) {
counts.set(endpoint, (counts.get(endpoint) ?? 0) + 1);
}
return [...counts.entries()]
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.slice(0, k)
.map(([endpoint]) => endpoint);
}
topEndpoints(
["/users", "/orders", "/users", "/health", "/orders"],
2
);
// ["/orders", "/users"] because the tie is alphabetical
For n requests and u unique endpoints:
k is small.Return paths whose values differ. Treat arrays as complete values.
function isObject(value) {
return value !== null &&
typeof value === "object" &&
!Array.isArray(value);
}
function differentPaths(before, after) {
const result = [];
function compare(left, right, path = "") {
const keys = new Set([
...Object.keys(left ?? {}),
...Object.keys(right ?? {})
]);
for (const key of keys) {
const nextPath = path ? `${path}.${key}` : key;
const leftHas = Object.hasOwn(left ?? {}, key);
const rightHas = Object.hasOwn(right ?? {}, key);
if (!leftHas || !rightHas) {
result.push(nextPath);
} else if (isObject(left[key]) && isObject(right[key])) {
compare(left[key], right[key], nextPath);
} else if (!Object.is(left[key], right[key])) {
result.push(nextPath);
}
}
}
compare(before, after);
return result.sort();
}
differentPaths(
{ server: { port: 3000 }, logging: true },
{ server: { port: 8080 }, logging: false, region: "us" }
);
// ["logging", "region", "server.port"]
For the interval problem, you might say:
“Let me confirm that touching intervals should merge and that I should not modify the input. I’ll sort a copy by start time. Then every interval only needs to be compared with the last merged interval. Sorting takes O(n log n), and the merge pass is O(n). I’ll test empty input, nested intervals, touching intervals, and non-overlapping intervals.”
This is concise but shows understanding, planning, testing, and complexity awareness.
Useful phrases:
Before the interview, test your camera, microphone, connection, and browser. Re-read the specific rules about references or AI tools; do not assume.
For each question:
The strongest preparation is not memorizing supposed Karat questions. Practise the complete process: understand, clarify, plan, implement, test, analyse, and adapt.
No approved comments are visible yet. New community replies may wait for moderation.