JavaScript interviews for freshers check that you can explain the basics in your own words, write a small function on a whiteboard and talk honestly about what you built. Expect questions on data types and typeof, null and undefined, NaN and decimal maths, modern syntax like spread and optional chaining, fetch and the DOM, a few short coding tasks and your college projects. It is written for final-year students, new graduates and anyone heading into a first JavaScript job from an internship or a bootcamp. Read each sample answer, then say your own version out loud. Closures, this, the event loop and promises in depth are on the main JavaScript page.
Search all questions by round, difficulty and level, or save the ones you want to practice.
Primitives: string, number, bigint, boolean, undefined, symbol and null; single values that can't be changed in place.
Objects: everything else, including arrays, functions and dates.
Quirks: typeof null is "object", an array is also "object", a function is "function".
Safe checks: Array.isArray for arrays and === null for null.
“JavaScript has seven primitive types: string, number, bigint, boolean, undefined, symbol and null. Primitives are single values that can't be changed in place, so any string method gives me a new string. Everything else is an object, and that includes arrays, functions and dates. The typeof operator has a few surprises. typeof null returns object, which is an old bug the language kept so it wouldn't break existing sites. An array also comes back as object, so to check for an array I use Array.isArray. A function is the one object that gets its own answer, function. So when I need to tell these apart, I check null with triple equals first, then use Array.isArray, and only then fall back to typeof.”
typeof "hi" // "string"
typeof 42 // "number"
typeof undefined // "undefined"
typeof null // "object"
typeof [1, 2] // "object"
typeof function () {} // "function"
Array.isArray([1, 2]) // true
Saying arrays are their own type in typeof, or not knowing that typeof null returns object.
| undefined | the default when a variable has no value yet, a property is missing or a function returns nothing. |
|---|---|
| null | an empty value a programmer sets on purpose. |
| not defined | a ReferenceError, because the name was never declared at all. |
“undefined is what JavaScript gives me by default. If I declare a variable and don't assign it, read a property that doesn't exist, or call a function that has no return, I get undefined. null is different because I set it myself, on purpose, to say there's no value here, like clearing the selected item in a list. Not defined isn't a value at all, it's a ReferenceError. It means I used a name that was never declared, and it's often just a typo. For comparisons, null double-equals undefined is true, but with triple equals it's false. So when I want to check for both at once, I either write value == null with a short comment, or use the nullish operator.”
let a;
console.log(a); // undefined
const user = { name: "Lee" };
console.log(user.age); // undefined
let picked = null; // cleared on purpose
console.log(score); // ReferenceError: score is not defined
Treating null and undefined as the same thing, or calling a ReferenceError an undefined value.
Falsy list: false, 0, -0, 0n, the empty string, null, undefined and NaN.
Everything else is truthy: including [], {}, "0" and "false".
Trap: if (count) skips a real count of zero, so check exactly what you mean.
“There's a short list of falsy values: false, zero, negative zero, the bigint zero, the empty string, null, undefined and NaN. Everything else is truthy. So an empty array passes an if check, and so does an empty object, and so do the strings zero and false, because they're non-empty strings. Where this bites people is a check like if (count) when zero is a valid count, or if (name) when an empty string should be allowed. In those cases I check exactly what I mean, like count !== undefined, or items.length > 0 when I want to know if an array has anything in it. To turn a value into a real boolean I use Boolean(value), which reads more clearly to most teammates than a double exclamation mark.”
Saying an empty array or an empty object is falsy.
!!value do, and why do people write it?stringify: turns a value into a JSON string for sending or storing.
parse: turns a JSON string back into values, and throws on bad input.
Losses: functions and undefined fields are left out, a Date becomes an ISO string, NaN becomes null.
“JSON.stringify turns a JavaScript value into a JSON string, which is what I send in a request body or save in localStorage. JSON.parse does the reverse. JSON only knows strings, numbers, booleans, null, arrays and plain objects, so some things don't survive. Functions and properties set to undefined are simply left out of objects. A Date becomes an ISO string, and after parse it stays a string, not a Date, so I have to convert it back myself. NaN and Infinity become null. On the parse side, JSON is stricter than JavaScript: keys need double quotes and trailing commas aren't allowed, and bad input throws a SyntaxError. So when I parse anything from outside, like a stored value or a server reply, I wrap it in try and catch.”
const data = { name: "Lee", joined: new Date(0), greet() {}, nick: undefined, score: NaN };
JSON.stringify(data);
// '{"name":"Lee","joined":"1970-01-01T00:00:00.000Z","score":null}'
JSON.parse('{"a": 1}'); // { a: 1 }
JSON.parse("{'a': 1}"); // SyntaxError: single quotes aren't JSON
Expecting a Date to come back as a Date after parse, or parsing outside data with no error handling.
What it is: a number value meaning a calculation had no valid numeric result, like 'abc' * 2.
Never equal: NaN isn't equal to anything, itself included.
Right check: Number.isNaN(x); the older global isNaN converts first, so isNaN('hello') is true.
“NaN stands for not a number, but funnily enough typeof NaN is number. It's what I get when a calculation can't produce a real number, like multiplying a word by two, or calling Number on text such as twelve px. The rule is that NaN isn't equal to anything, not even itself, so a triple-equals check never finds it. The proper check is Number.isNaN. There's also an older global isNaN, but it converts the value to a number first, so isNaN('hello') returns true even though that's a string, not NaN. In a form, I'd convert the input with Number, then use Number.isNaN to show an error before the bad value spreads into a total and turns everything it touches into NaN.”
Number("12px") // NaN
parseInt("12px") // 12
NaN === NaN // false
Number.isNaN(NaN) // true
isNaN("hello") // true, it converts first
Number.isNaN("hello") // false
Object.is(NaN, NaN) // true
Checking with x === NaN, or trusting the global isNaN on strings.
Number('12px') and parseInt('12px')?[NaN].includes(NaN) find it? What about [NaN].indexOf(NaN)?Cause: every number is a 64-bit binary floating point value, and 0.1 has no exact binary form.
Result: the sum comes out as 0.30000000000000004.
Compare: check that the difference is below a small tolerance.
Display or store: round with toFixed for display, or do the maths in whole units like cents.
“All numbers in JavaScript are 64-bit floating point, stored in binary. Just like one third can't be written exactly in decimal, 0.1 and 0.2 can't be written exactly in binary, so each is stored as the closest value, and the tiny errors add up. The sum comes out as 0.30000000000000004, so a triple-equals check fails. It isn't a JavaScript bug; other languages using the same number format do exactly the same. To compare, I check that the difference is smaller than a small tolerance, like Number.EPSILON for values around one. For showing a result I round with toFixed, keeping in mind it returns a string. And for prices, I'd do the maths in whole cents as integers, so nothing needs rounding until I display it.”
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
(0.1 + 0.2).toFixed(2) // "0.30", a string
const totalCents = 10 + 20; // 30, exact
Calling it a JavaScript bug, or rounding with toFixed and then comparing the strings.
toFixed return a string, and what trouble can that cause?Primitives: the function gets a copy of the value, so changes stay inside.
Objects: the function gets a copy of the reference, so both point at the same object.
Mutate vs reassign: changing a property shows outside; pointing the parameter at a new object doesn't.
“With a number or a string, the function gets its own copy of the value, so whatever I do inside stays inside. With an object or an array it's more subtle. The variable holds a reference, and the function gets a copy of that reference. So the caller and the function both point at the same object. If I change a property, or push into the array, the caller sees it. But if I reassign the parameter to a brand new object, that only changes the function's local name, and the caller still has the original. That's why people call it pass by sharing. In practice I try not to surprise callers: if a function needs a changed version, I return a new object built with spread instead of editing the one passed in.”
function update(n, user) {
n = n + 1; // local copy only
user.name = "Kim"; // same object as the caller's
user = { name: "New" }; // local name now points elsewhere
}
let count = 1;
const person = { name: "Lee" };
update(count, person);
console.log(count, person.name); // 1 "Kim"
Saying objects are passed by reference so reassigning the parameter changes the caller's variable.
const stop you from changing an object's properties?Definition: a function you pass to another function, which calls it for you.
Right away: a sort comparator runs during the sort call itself.
Later: setTimeout or addEventListener call it after the current code finishes.
Why promises came: nested async callbacks get hard to read and to handle errors in.
“A callback is just a function I hand to another function so it can call it for me. Functions are values in JavaScript, so I can pass them around like anything else. Some callbacks run right away. When I call sort with a comparator like (a, b) => a - b, sort calls it many times before it returns. That comparator matters, because sort without one compares items as strings, so ten comes before two. Other callbacks run later. With setTimeout or a click listener, my function is stored and called after the current code finishes, when the timer fires or the user clicks. When several async steps depend on each other, those callbacks nest deeper and deeper, and handling errors gets messy. That's the problem promises and async/await were built to fix.”
const nums = [10, 2, 33];
nums.sort(); // [10, 2, 33], compared as strings
nums.sort((a, b) => a - b); // [2, 10, 33], callback runs during sort
console.log("first");
setTimeout(() => console.log("third"), 1000); // runs later
console.log("second");
Saying every callback is asynchronous, or not being able to name a callback you've written.
Spread: expands an array or object into single items, in calls, array literals and object literals.
Rest: collects the leftover items into an array or object, in parameters and destructuring.
Destructuring: pulls values out by position or by name; a default kicks in only for undefined.
“The same three dots do two opposite jobs depending on where they sit. Spread expands things: Math.max(...nums) passes each number as its own argument, and { ...user, role: 'admin' } copies an object and overrides one field. Rest collects things: in a function's parameters, ...rest gathers every extra argument into a real array, and in destructuring it gathers whatever I didn't name. Destructuring itself is a short way to pull values out, by position for arrays and by name for objects. I can give a default, like pages = 100, but it only applies when the value is undefined, not when it's null. A neat trick it enables is swapping two variables in one line without a temporary variable.”
const nums = [4, 9, 2];
Math.max(...nums); // 9, spread into arguments
const user = { name: "Lee", role: "viewer" };
const admin = { ...user, role: "admin" }; // copy, then override
function sum(first, ...rest) { // rest collects extras
return rest.reduce((a, b) => a + b, first);
}
const { title, pages = 100 } = { title: "Notes" }; // pages is 100
const [head, ...tail] = [1, 2, 3]; // head 1, tail [2, 3]
let a = 1, b = 2;
[a, b] = [b, a]; // swap
Mixing up spread and rest, or expecting a default value to replace null.
volume ?? 10 often safer than volume || 10?Optional chaining: a?.b gives undefined if a is null or undefined, instead of throwing.
Nullish coalescing: x ?? y falls back only when x is null or undefined.
Versus ||: || falls back on any falsy value, so it replaces a real 0 or an empty string.
“Optional chaining, the question mark dot, lets me read deep into an object without crashing. user?.address?.city gives me undefined if user or address is missing, instead of throwing 'cannot read properties of undefined'. It works for calls too, like onDone?.(). The double question mark is nullish coalescing: it uses the right side only when the left side is null or undefined. That's the difference from the OR operator, which falls back on any falsy value. So if a user sets the volume to zero, volume || 10 quietly turns it back into ten, which is a bug, while volume ?? 10 keeps the zero. One caution: I don't sprinkle question mark dots everywhere, because if a value should always exist, I'd rather get a clear error than hide a real problem.”
const settings = { volume: 0, theme: "" };
settings.volume || 10 // 10, the zero was lost
settings.volume ?? 10 // 0
settings.theme ?? "light" // "", empty string kept
const user = {};
const city = user?.address?.city; // undefined, no crash
const onDone = undefined;
onDone?.(); // skipped, no error
Using || for defaults where zero or an empty string is valid, or adding ?. everywhere to silence errors.
?. protect you from a variable that was never declared?a || b ?? c give a syntax error?| for...in | loops over an object's enumerable property keys as strings, including inherited ones. |
|---|---|
| for...of | loops over the values of an iterable, such as an array, string, Map or Set. |
Plain objects: aren't iterable, so use Object.entries with for...of.
“for...in walks over keys. On an object it gives me each enumerable property name, and it also includes properties inherited through the prototype, which is a surprise if a library added one. On an array it gives me the indexes, but as strings, and it'll also pick up any extra property someone attached to the array. for...of walks over the values of anything iterable: arrays, strings, Maps and Sets. So for an array I use for...of, or a normal for loop if I need the index. A plain object isn't iterable, so for...of on it throws a TypeError. For objects I use Object.entries with for...of and destructure the key and the value, which also gives me only the object's own properties.”
const scores = [90, 75];
for (const i in scores) console.log(typeof i); // "string" twice
for (const s of scores) console.log(s); // 90, then 75
const ages = { sam: 21, lee: 23 };
for (const [name, age] of Object.entries(ages)) {
console.log(name, age);
}
// for (const x of ages) {} -> TypeError: ages is not iterable
Using for...in to loop over array values, or trying for...of on a plain object.
Turning it on: the string at the top of a file or function; ES modules and classes are strict already.
Undeclared variables: assigning to one throws a ReferenceError instead of creating a global.
this: in a plain function call it's undefined, not the global object.
Silent failures: writing to a read-only property throws instead of being ignored.
“Strict mode is an opt-in that makes JavaScript less forgiving. I turn it on with the string 'use strict' at the top of a file or a function, though ES modules and class bodies are strict automatically. The biggest change is that assigning to a variable I never declared throws a ReferenceError. Without it, a typo like totl = 5 silently creates a global variable, which is a nasty bug to find later. Second, inside a plain function call, this is undefined instead of the global object, so a mistake fails loudly instead of quietly changing global state. It also throws when I write to a read-only property, and it bans duplicate parameter names. I like it because it turns silent mistakes into clear errors early.”
Thinking strict mode adds type checking or makes the code run faster.
Request: await fetch(url) gives a Response once the server answers.
Status check: fetch only rejects on network failure, so check res.ok yourself.
Body: await res.json() reads and parses the body; it's a second promise.
States: show loading, then the data or a friendly error.
“I'd write an async function that awaits fetch with the URL. That gives me a Response object as soon as the server answers. The part beginners miss is that fetch only rejects when the request can't complete at all, like the network being down. A 404 or a 500 still resolves normally, so I check res.ok and throw my own error if it's false. Then I await res.json(), which is a second promise because the body arrives after the headers. I wrap it all in try and catch, so a failure shows a friendly message instead of a blank page. I put the data on the page with textContent, not innerHTML, so text from the server can't run as code. In my project I also showed a loading message while waiting.”
async function loadUsers(list, message) {
message.textContent = "Loading...";
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const users = await res.json();
list.textContent = users.map((u) => u.name).join(", ");
message.textContent = "";
} catch (err) {
console.error(err);
message.textContent = "Could not load users. Please try again.";
}
}
Assuming the catch block handles a 404, or forgetting that res.json() needs await too.
Wrap the timer: create a promise and pass resolve to setTimeout.
Await in a loop: a normal for loop with await runs one step at a time.
Trap: forEach doesn't wait for async callbacks.
“setTimeout works with a callback, so I wrap it in a promise. The sleep function takes milliseconds and returns a new Promise, and inside it I call setTimeout with resolve as the callback, so the promise settles once the time is up. Then I write an async function with a plain for loop from one to three. Each time round, it awaits sleep of a thousand milliseconds, then logs the number. The key detail is using for or for...of, not forEach. forEach doesn't wait for promises, so with an async callback all three timers would start together and the numbers would print at almost the same moment after one second. Sleep never rejects, so I don't need a catch here, but with real network calls in the loop I'd add one.”
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function countUp() {
for (let i = 1; i <= 3; i++) {
await sleep(1000);
console.log(i);
}
}
countUp(); // 1, 2, 3, one second apart
Writing a busy while loop to wait, which freezes the page, or expecting forEach to wait for each step.
DOM: the browser's tree of objects built from the HTML, which JavaScript can read and change.
Finding: querySelector returns the first match or null; querySelectorAll returns a NodeList.
Changing: textContent sets plain text; innerHTML parses the string as HTML.
Safety: user input inside innerHTML can run injected script.
“The DOM is the tree of objects the browser builds from the HTML. Each tag becomes a node, and JavaScript can read it, change it, add to it or remove from it, and the page updates. To find elements I mostly use querySelector with a CSS selector, which gives me the first match or null, and querySelectorAll, which gives me a NodeList of all matches. getElementById still works and is fine for one element by id. To change text I set textContent. innerHTML parses whatever I give it as HTML, so if that string came from a user, like a comment box, someone could inject an image tag with an onerror handler and run their own script. That's cross-site scripting. So user text goes in textContent, and I only use innerHTML with markup I control.”
const box = document.querySelector("#greeting");
const input = document.querySelector("#name");
box.textContent = `Hello, ${input.value}`; // shown as plain text
// box.innerHTML = input.value; // risky: an <img onerror=...> would run
const items = document.querySelectorAll(".item"); // NodeList
items.forEach((el) => el.classList.add("done"));
Putting user input straight into innerHTML, or not knowing that querySelector returns null when nothing matches.
Cause: a script in the head runs before the body is parsed, so the button doesn't exist yet.
Fix: add defer so the script runs after parsing, in order.
Other options: put the script at the end of the body, or wait for DOMContentLoaded.
Also check: a typo in the selector, like a missing #.
“The browser reads HTML from top to bottom, and a normal script tag pauses parsing and runs straight away. If my script is in the head, it runs before the body has been read, so the button doesn't exist yet and querySelector returns null. Then calling addEventListener on null throws. The cleanest fix is the defer attribute: the script downloads in parallel but runs only after the document is parsed, and deferred scripts keep their order. Other options are moving the script to the end of the body, or wrapping the code in a DOMContentLoaded listener. The async attribute is different: it runs as soon as it's downloaded, so it could still run too early. And before blaming timing, I'd check the selector itself, like a missing hash or a typo in the id.”
Fixing it with a setTimeout delay and hoping the page is ready by then.
| localStorage | stays until cleared, shared by every tab of the same origin. |
|---|---|
| sessionStorage | belongs to one tab and is gone when that tab closes. |
| Cookies | small, sent to the server with matching requests, can expire and be HttpOnly. |
Strings only: storage holds strings, so objects go through JSON.
“All three keep data in the browser, but they behave differently. localStorage keeps data until someone clears it, and every tab from the same origin, meaning the same protocol, domain and port, sees it. sessionStorage belongs to one tab and disappears when that tab closes. Cookies are small and, importantly, get sent to the server with every matching request, which is why they're used for login sessions, and the server can mark them HttpOnly so JavaScript can't read them at all. For a theme choice, localStorage is a good fit: it's small, the server doesn't need it and it should survive a restart. Two things I remember: storage only holds strings, so I use JSON.stringify and JSON.parse for objects, and I'm careful with login tokens there, because any script running on the page can read them.”
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark"
localStorage.setItem("prefs", JSON.stringify({ fontSize: 16 }));
const prefs = JSON.parse(localStorage.getItem("prefs"));
sessionStorage.setItem("step", "2"); // gone when the tab closes
Thinking localStorage is sent to the server, or saving an object without JSON and getting back "[object Object]".
Clean: lowercase, then keep only letters and digits with a regex.
Compare: reverse and compare, or walk two pointers inward.
Edge cases: empty string, one character, mixed case.
“First I'd clean the input: lowercase it and remove anything that isn't a letter or a digit with a regular expression, so the car and cat sentence becomes one long lowercase string. The quick version splits it into characters, reverses, joins and compares. It's easy to read but builds a reversed copy. The version I'd write on the board uses two pointers, one at the start and one at the end, moving inward and returning false at the first mismatch. It stops early and needs no reversed copy. I'd mention edge cases out loud: an empty string or a single character counts as a palindrome. And this regex only keeps plain a to z letters, so I'd call that out if the input could contain accented letters.”
function isPalindrome(text) {
const s = text.toLowerCase().replace(/[^a-z0-9]/g, "");
let left = 0;
let right = s.length - 1;
while (left < right) {
if (s[left] !== s[right]) return false;
left++;
right--;
}
return true;
}
isPalindrome("Was it a car or a cat I saw?"); // true
isPalindrome("hello"); // false
Forgetting to clean the string first, so 'Racecar' or a sentence with commas fails.
Set: [...new Set(arr)] keeps the first of each value, in order.
Without Set: loop once and track values you've seen in a lookup object.
Cost: filter with indexOf is O(n squared); a lookup keeps it O(n).
“The one-liner is to put the array into a Set, which only keeps unique values, and spread it back into an array. It keeps the first time each value appears, in the original order. Without Set, the classic answer is filter with indexOf, keeping an item only if its first index equals the current index. It's short, but indexOf scans the array each time, so it's O(n squared) and slow on big lists. A better manual version loops once and uses an object as a lookup of values I've already seen, pushing only new ones into the result. That's O(n). I'd also mention that Set compares objects by reference, so two different objects with the same fields would both stay.”
const nums = [3, 1, 3, 2, 1];
const unique = [...new Set(nums)]; // [3, 1, 2]
function dedupe(arr) {
const seen = {};
const out = [];
for (const n of arr) {
if (!seen[n]) {
seen[n] = true;
out.push(n);
}
}
return out;
}
dedupe(nums); // [3, 1, 2]
Only knowing the Set trick and not being able to explain what it does or what the manual version costs.
Count: one pass builds a map of how often each character appears.
Find: a second pass, in string order, returns the first character with a count of one.
Cost: O(n) time; the map holds one entry per distinct character.
“I'd do it in two passes. In the first, I loop over the string and count each character in a Map, adding one every time I see it. In the second pass, I loop over the string again, in order, and return the first character whose count is exactly one. For 'swiss', s appears three times, w and i once each, and w comes first, so the answer is w. If nothing is unique I return null, and I'd ask the interviewer whether uppercase and lowercase should count as the same letter. It's O(n) time. The common slow approach is nesting two loops to compare every pair, which works but is O(n squared). I use a Map rather than a plain object because it's built for lookups like this and has no inherited keys to worry about.”
function firstUnique(str) {
const counts = new Map();
for (const ch of str) {
counts.set(ch, (counts.get(ch) || 0) + 1);
}
for (const ch of str) {
if (counts.get(ch) === 1) return ch;
}
return null;
}
firstUnique("swiss"); // "w"
firstUnique("aabb"); // null
Using nested loops without noticing the cost, or returning the first character from the map instead of from the string.
Two trackers: keep first and second, both starting at -Infinity.
Update: a bigger number pushes first down to second; a number between them replaces second.
Duplicates: skip values equal to first, so [5, 5, 3] gives 3.
No answer: return null when there's no distinct second value.
“Sorting works but it's O(n log n), and the default sort compares numbers as strings unless I pass a comparator. In one pass, I keep two variables, first and second, both starting at negative infinity. For each number, if it's bigger than first, the old first moves down to second and the number becomes first. Otherwise, if it's smaller than first but bigger than second, it becomes second. Using strictly smaller than first matters: with five, five, three, the second five doesn't overwrite second, so the answer is three, which is usually what's wanted, though I'd confirm it. At the end, if second is still negative infinity, there's no distinct second value, like an array with one element or all the same number, so I return null.”
function secondLargest(nums) {
let first = -Infinity;
let second = -Infinity;
for (const n of nums) {
if (n > first) {
second = first;
first = n;
} else if (n < first && n > second) {
second = n;
}
}
return second === -Infinity ? null : second;
}
secondLargest([4, 9, 2, 9]); // 4
secondLargest([7]); // null
Sorting without a comparator, or returning the largest number again when it appears twice.
nums.sort()[nums.length - 2] give the wrong answer for [9, 10]?Why === fails: it compares references, so two separate objects with the same content aren't equal.
Base cases: equal with ===, true; either not an object or null, false.
Recurse: same number of keys, then compare each key's value deeply.
Limits: say what you skip, such as Dates, Maps, NaN and objects that refer to themselves.
“Triple equals on objects compares references, so two separate objects with identical content are still not equal. My function starts with a quick check: if the two values are triple-equal, return true. If either one isn't an object, or is null, they can't be deeply equal, so return false. I also return false when one is an array and the other isn't. Then I get both lists of keys with Object.keys. If the counts differ, false. Otherwise, for every key, I check the other object has it as its own key, and call the function again on the two values, stopping at the first mismatch. Arrays work too, because their indexes are keys. I'd tell the interviewer what this skips: Dates, Maps, NaN and objects that refer to themselves.”
function deepEqual(a, b) {
if (a === b) return true;
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
return false;
}
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key]));
}
deepEqual({ x: [1, { y: 2 }] }, { x: [1, { y: 2 }] }); // true
deepEqual({ x: 1 }, { x: "1" }); // false
Comparing JSON.stringify strings and calling it done, which fails when the same keys come in a different order.
The project: what it did and who used it, in two sentences.
Your part: the specific pieces you wrote, not the team's whole feature list.
The bug: what broke, how you narrowed it down, the fix.
Looking back: one honest thing you'd do differently.
“In my final year I built a small expense-splitting web app with two classmates, using plain JavaScript on the front end and a simple Node API. I owned the front end: the form to add expenses, the list and the running balances. The hardest bug was that clicking delete sometimes removed the wrong expense. I stepped through it with breakpoints in the browser dev tools and saw that each delete button remembered its position in the list from when it was first drawn, so after one delete the positions shifted. I fixed it by giving every expense an id, storing it on the button in a data attribute and deleting by id instead. Looking back, I'd write a few small tests for the balance maths, because I checked those by hand every time.”
Describing only what the team built, or being unable to explain how your own part of the code works.
Talk to them: raise it with the teammate directly, not in front of the group.
Understand it: read it together until someone can explain every line.
Check the risks: innerHTML with user input, stray globals, and whether the source or your course allows reuse.
Decide: keep it with a comment and a credit, or rewrite the small part you really need.
“I wouldn't rip it out or make a fuss in the group chat. First I'd sit down with my teammate and read it together, stepping through it in the browser dev tools if that helps, until at least one of us can explain every line. Working code that nobody understands is a risk. If it breaks the night before the demo, nobody can fix it, and the examiner might ask any of us how it works. I'd also check a few basics: does it put user input into innerHTML, does it create global variables, and are we allowed to reuse it under the source's licence and our course rules. If it passes, we add a short comment saying what it does and where it came from. If it's too tangled, I'd suggest we rewrite just the part we need, smaller and in our own style.”
Keeping code nobody can explain just because it works, or calling the teammate out in front of the group.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.