Skip to content
Skilled.dev

Interactive tools

JavaScript output quiz: 12 snippets with explained answers

Predict JavaScript output for closures, this, copies, Map keys, defaults, sorting, and promises. Compare explanations and retry missed snippets.

Published ยท 5 min read

Output question 1 of 12

Each snippet runs independently inside a strict-mode function. Wait for its promise jobs to finish. Each output line represents one console.log call; spaces separate its arguments.

A closure reads a changed binding

let count = 1;
const read = () => count;
count = 3;
console.log(read());
What is logged?
Read all 12 snippets, outputs, and explanations

Use this reference after trying a round, or read the traces directly.

1. A closure reads a changed binding

let count = 1;
const read = () => count;
count = 3;
console.log(read());

Output:

3

The arrow retains access to the surrounding count binding. The assignment changes that same binding before read() runs. A closure can preserve access to a variable without preserving its old value.

  • 1: Creating the arrow does not take a snapshot of count.
  • undefined: count is initialized and remains in scope.
  • ReferenceError: The binding is accessible from the arrow.

Try a change: Assign count to a new const before creating the arrow. Which binding must the arrow read to preserve 1?

Read javascript preparation. MDN: closures.

2. Loop callbacks with let

const reads = [];
for (let i = 0; i < 3; i += 1) reads.push(() => i);
console.log(reads.map((read) => read()).join(','));

Output:

0,1,2

A for loop with this let declaration creates a fresh iteration binding. The callbacks run later, but they read different bindings holding 0, 1, and 2. Delayed execution alone does not imply that every closure shares one variable.

  • 3,3,3: That is the result of a shared var binding, not this let loop.
  • 1,2,3: The callback closes over the current iteration before the next increment.
  • (empty line): Three callbacks are pushed and map invokes each one.

Try a change: Replace let with var and explain the next question before checking it.

Read javascript preparation. MDN: closures in loops.

3. Loop callbacks with var

const reads = [];
for (var i = 0; i < 3; i += 1) reads.push(() => i);
console.log(reads.map((read) => read()).join(','));

Output:

3,3,3

The callbacks share one function-scoped i. The loop finishes when i reaches 3, and only afterward does map call the callbacks. Each invocation reads that final value. No timers are needed to produce this behavior.

  • 0,1,2: var does not create a new binding for each iteration.
  • 1,2,3: All reads happen after the loop, not between its increments.
  • ReferenceError: The function-scoped var remains accessible after the loop.

Try a change: Pass i into a callback factory with a parameter. Explain why that also separates the saved values.

Read javascript preparation. MDN: var.

4. A method loses its receiver

const box = {
  value: 7,
  read() { return this === undefined ? 'unbound' : this.value; }
};
const read = box.read;
console.log(box.read(), read());

Output:

7 unbound

For an ordinary function, the invocation determines this. The property call uses box as receiver. The detached plain call has an undefined receiver under the stated strict-mode contract. The function body handles that case explicitly.

  • 7 7: Saving a function in read does not bind it to box.
  • 7 undefined: The explicit guard returns the string unbound for an undefined receiver.
  • TypeError: The guard avoids reading this.value when this is undefined.

Try a change: Replace the assignment with box.read.bind(box). Which call forms then return 7?

Read javascript function context. MDN: this.

5. An arrow keeps its enclosing receiver

function makeReader() { return () => this.value; }
const read = makeReader.call({ value: 4 });
console.log(read.call({ value: 9 }));

Output:

4

makeReader is an ordinary function, so call supplies its receiver. The arrow captures that invocation's this. A later call on the arrow can pass arguments but cannot replace its captured receiver.

  • 9: call cannot replace the lexical this of an arrow.
  • undefined: makeReader receives an explicit object with a value property.
  • TypeError: Calling an arrow with call is legal; its receiver argument is ignored.

Try a change: Return an ordinary function instead of an arrow. What does the final call print?

Read javascript function context. MDN: arrow functions.

6. Object spread shares a nested object

const original = { nested: { count: 1 }, label: 'a' };
const copy = { ...original };
copy.nested.count = 2;
copy.label = 'b';
console.log(original.nested.count, original.label);

Output:

2 a

Spread copies these enumerable own property values into a new outer object. The nested property value is a reference to the same object. The label value is a string; replacing that property on copy leaves the property on original unchanged.

  • 1 a: The new outer object still holds the same nested object reference.
  • 2 b: The outer objects differ, so assigning copy.label does not change original.label.
  • TypeError: const prevents rebinding, not ordinary property mutation.

Try a change: Copy nested with another spread. Which deeper references would still need attention?

Read collections and readonly contracts. MDN: spread syntax.

7. Numeric values with the default sort

const values = [2, 11, 3];
values.sort();
console.log(JSON.stringify(values));

Output:

[11,2,3]

Without a comparator, sort orders these values by their string forms. The string 11 begins with 1 and comes before 2. The array is mutated, while its elements remain numbers. JSON.stringify fixes the display format instead of relying on a console's object inspector.

  • [2,3,11]: Numeric ascending order requires an appropriate comparator.
  • [2,11,3]: sort mutates this array; its result is not discarded by ignoring the return value.
  • ["11","2","3"]: String comparison does not replace these stored numbers with strings.

Try a change: Add (a, b) => a - b. Then preserve the input by sorting a shallow array copy.

Read searching and sorting. MDN: Array.sort.

8. Object keys in a Map

const key = { id: 1 };
const values = new Map([[key, 'saved']]);
console.log(values.has({ id: 1 }), values.has(key));

Output:

false true

Each object literal creates a distinct object. Map compares object keys by identity, so a new object with the same id does not find the entry. The saved key variable does. Use primitive IDs deliberately when the task needs equality by identifier.

  • true true: Matching properties do not make distinct objects the same Map key.
  • false false: Map supports object keys and retains the original reference.
  • true false: Map does not coerce these object keys to property-name strings.

Try a change: Compare new Map([[NaN, 1]]).has(NaN) with NaN === NaN. Which equality rule does Map use?

Read collection key identity. MDN: Map key equality.

9. Null, zero, and defaulting rules

const { count = 5 } = { count: null };
console.log(count, 0 || 5, 0 ?? 5);

Output:

null 5 0

The property is null, so its destructuring default is unused. Logical OR treats zero as falsy and returns 5. Nullish coalescing preserves zero because zero is neither null nor undefined. Choosing an operator requires deciding which missing-value contract the data uses.

  • 5 5 5: Neither destructuring defaults nor ?? replace every falsy value.
  • null 0 0: Zero is falsy, so || selects its right operand here.
  • 5 5 0: A destructuring default does not replace an explicitly present null.

Try a change: Change count to undefined and replace both zero operands with an empty string. Trace each result.

Read missing-value contracts. MDN: nullish coalescing.

10. A fulfilled promise still schedules its handler

console.log('A');
Promise.resolve().then(() => console.log('B'));
console.log('C');

Output:

A
C
B

A and C are logged during the current synchronous execution. then queues a reaction for the fulfilled promise, and that reaction later logs B. There is no timer deadline to estimate in this example.

  • A B C: A then handler does not run inline just because the promise is already fulfilled.
  • B A C: Scheduling a handler does not run it before earlier synchronous statements.
  • A C: The handler runs after synchronous execution; it is not cancelled.

Try a change: Schedule another fulfilled-promise handler after logging C. Which reaction was enqueued first?

Read event-loop execution traces. MDN: Promise.then.

11. An await of a non-promise value

async function work() {
  console.log('start');
  await 0;
  console.log('resume');
}
work();
console.log('end');

Output:

start
end
resume

The work call logs start before reaching await. Await turns the value into a fulfilled promise and suspends the function. The caller logs end while the continuation waits. The continuation then logs resume. An async function is not a separate thread.

  • end start resume: Calling the async function begins its body synchronously.
  • start resume end: Await suspends this function even for the plain value 0.
  • TypeError: Await accepts non-promise values.

Try a change: Remove await but retain async. Does the ordering of the two body logs change?

Read async execution traces. MDN: await.

12. A successful finally preserves fulfillment

Promise.resolve(2)
  .finally(() => 9)
  .then((value) => console.log(value));

Output:

2

finally is useful for cleanup that should preserve the prior outcome when cleanup succeeds. Returning 9 succeeds, and the following handler receives 2. If the callback throws or returns a rejected promise, the resulting rejection changes the chain instead.

  • 9: A successful finally callback does not replace fulfillment with its ordinary return value.
  • undefined: The prior fulfillment value is forwarded through this successful finally.
  • 2 9: There is only one log call, in the final then handler.

Try a change: Replace finally with then. Then restore finally and make its callback throw; add a rejection handler.

Read promise behavior and aggregation. MDN: Promise.finally.

These examples are displayed as text. Answers stay in this page and reset on reload. No account is required.

Predict the logged output, check the explanation, and then change one detail in the snippet. The quiz covers twelve examples involving closure bindings, loop scopes, function receivers, shallow copies, collection identity, defaulting operators, sorting, and promise execution. A correct choice is a starting point: the explanation should account for every logged value and its position.

Use a precise execution contract

Each snippet runs independently inside a strict-mode function. This removes differences caused by top-level script bindings, module scope, and a host's global object. In particular, a detached ordinary function receives undefined as its receiver in the receiver question. The snippet explicitly handles that value so the answer does not depend on a host's exception wording.

Wait for the snippet's promise jobs to finish before deciding its complete output. Each displayed output line represents one console.log call, and spaces separate that call's arguments. Examples log primitive values or an explicitly serialized array. A browser's expandable object inspector can show a later object state or format values differently; that presentation is outside these questions' contract.

Trace bindings before evaluating expressions

For a closure, identify the binding the function reads and when it reads it. Creating an arrow that refers to count does not copy count's current value. If the same binding changes before the arrow is called, the later read sees the changed value. The JavaScript preparation guide connects this behavior to reusable functions and interview implementation choices.

The two loop questions deliberately use the same callback body with different declarations. The let loop provides separate iteration bindings, while the var loop shares one function-scoped binding. The callbacks execute after the loop in both examples. Delayed execution explains the time of the reads; the declaration explains whether they read one binding or several. Neither example requires a timer.

Separate a function from its receiver

An ordinary function's receiver depends on its invocation. Saving a method into another variable preserves the function object but does not bind it to the original owner. A property call and a detached plain call can therefore run the same body with different receiver values. Binding with bind or invoking with call changes that relationship deliberately.

An arrow uses the receiver from its surrounding lexical environment. Calling the arrow with call does not replace that captured receiver. For each example, name the ordinary function invocation that supplies this, then identify which functions inherit it. This is more reliable than treating every dot near a function as evidence of a permanent binding.

Inspect identity, copies, and missing values

Object spread creates a new outer object but copies the nested reference in the copy question. A mutation through that shared reference reaches the same nested object. Assigning a new outer label only changes the copied object's property. Draw two outer boxes pointing to one nested box before trying the follow-up that copies another level.

Map's object keys also depend on reference identity. A fresh object with matching fields is a different key. If an algorithm means to group entries by an ID, choose that primitive ID as the key explicitly. The TypeScript collections guide discusses these runtime rules alongside missing-value and readonly contracts; its TypeScript annotations do not change object identity.

Defaulting expressions require their own contract. Destructuring uses its default when a value is undefined. Logical OR selects its fallback for any falsy left operand. Nullish coalescing selects its fallback for null or undefined. A count of zero and an explicitly supplied null can therefore behave differently even when several expressions are described casually as defaults.

Write a queue trace for asynchronous examples

Begin by listing the synchronous statements in order. When a fulfilled promise receives a then handler, record that reaction as queued work and continue the synchronous trace. A fulfilled state does not make the handler execute inline. For await, trace the function body until suspension, return to its caller, and then trace the continuation when its job runs.

The finally question tests value propagation separately from ordering. A successful cleanup callback preserves the earlier outcome, even when its ordinary return value differs. A thrown error or rejected promise can change the outcome. These cases help when debugging promise chains; the event-loop guide and Promise.all implementation provide longer traces and aggregation practice.

Review mistakes with a changed example

After checking an answer, read why your selected alternative differs from the actual trace. Finish the round to retry only missed snippets, or open the complete reference to compare all alternatives. Scores measure these twelve prompts, and memorizing their outputs does not establish that the underlying concepts transfer to unfamiliar code.

Use each follow-up to test that transfer. Replace an arrow with an ordinary function, change a missing value to zero, add a numeric sort comparator, or make cleanup reject. Predict the changed output before running it in your own environment under the stated contract. The complete reference includes links to the relevant language documentation for checking a disputed rule.

The application displays these reviewed snippets as text; the quiz does not execute entered programs. Answers remain in the page and reset on reload. It needs no account, produces no public answer URLs, and can be used on a self-hosted instance. After a round, move to the JavaScript course material or build a short review schedule with the study-plan builder.

Practice in the course

Open a lesson to work through its explanation and examples. Coding exercises include an editor; available code languages are shown in each lesson.

Continue learning