CSCI2720 · Stage practice A: explanations and marking guide
Attempt the paper before reading this guide. Award each reasoning point for a correct explanation of the stated program, including an equivalent explanation expressed with a clear state table, reference diagram or trace.
Q01 — Resolve a resource and explain the request (7 marks)
https://notes.example.test:8443/course/assets/timetable.svg?size=2#caption
https://notes.example.test:8443
/course/assets/timetable.svg?size=2
(a) The three lines answer three different questions: “What is the complete address?”, “Which origin does it belong to?”, and “Which path and query identify the requested resource?” The URL constructor parses the address into fields. Merely constructing that object does not load the resource.
Read the resolved address from left to right:
| Field | Value in this question | Meaning |
|---|---|---|
| Scheme | https | The address uses HTTPS. |
| Hostname | notes.example.test | The server name in the address. |
| Port | 8443 | The explicitly selected port; it is not HTTPS's default port 443. |
| Path | /course/assets/timetable.svg | The resource's location within this server's URL space. |
| Query | size=2 | Additional request data, preceded by ? in the serialized URL. |
| Fragment | caption | A location or identifier within the resource, preceded by #. |
target.href includes all those fields in one string. target.origin retains the scheme, hostname and explicit nondefault port, giving https://notes.example.test:8443; it excludes the path, query and fragment. Distinguishing the origin is useful when deciding whether two addresses refer to the same scheme/host/port environment. Two resources can share an origin while having different paths. Changing the port can change the origin even when the hostname is unchanged.
(b) Begin with the base document, not with a guessed server file path. Its path is /course/week3/index.html. For relative resolution, the last component is the document name, so the containing directory is /course/week3/. The first .. in ../assets/timetable.svg then moves up one directory to /course/. Appending assets/timetable.svg gives /course/assets/timetable.svg. The scheme, hostname and port are inherited from the base because the relative address does not replace them.
The parsed pathname is /course/assets/timetable.svg. The search property includes its leading punctuation, so it is ?size=2. Concatenating those two strings produces the third output line; the program does not have to add another question mark. The fragment is stored separately and is not included in an ordinary HTTP request target.
This distinction tells us where to look when diagnosing a request. A server handling that path can inspect the query size=2. It does not receive #caption as part of the request target. The browser can use the fragment after obtaining the resource. Treating every character in the visible address as server input would therefore attribute a browser navigation decision to the wrong part of the system.
(c) Trace each operation through its owner. Changing only the fragment can move to another location in the existing document. A handler assigning to a paragraph's textContent changes a DOM node in the current browser document. That DOM node is the immediate result of the second operation; it is not itself a record on a server.
To claim that a server saved new data, we would need a further chain: the browser sends the data, a service accepts it, and its completed storage operation has the required lifetime. A visible text change establishes none of those later steps by itself. If the handler only updates a variable and the DOM, a new document starts from its own initialization. When explaining an interface, distinguish a successful local update from a completed save rather than using the word “saved” for both.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark for each exact output line, including the port and fragment placement. |
| b | 2 | One mark for resolving the parent directory correctly; one for distinguishing request query data from the fragment. |
| c | 2 | One mark for identifying document-local navigation or DOM work; one for explaining that server persistence requires an actual communication/storage operation. |
Q02 — Read a document's meaning (8 marks)
(a) title provides the document title used by the browsing interface, such as its tab. h1 is the visible top-level heading for the page's content. main identifies the document's main content region. The identical wording of title and h1 does not make them interchangeable: they occupy different places and have different roles.
These elements give a reader and a tool different ways to identify the same document. The tab title helps distinguish this document from other open documents; the heading introduces the content after entering it; the main region locates the primary material within the document. Choosing by role keeps those relationships meaningful even if CSS later changes their appearance.
(b) The data cell states that the CSS study session takes 25 minutes. CSS is the header for that row because it uses scope="row"; Minutes is the header for that column because it uses scope="col". The caption, Study sessions, names the whole table. The link's #schedule fragment identifies the element with id="schedule", here the table. It is a destination within this document rather than another independent document resource.
To reconstruct the cell's meaning, locate its row first: its row header names CSS. Then move up its column: its column header supplies the unit, minutes. Combining them yields “CSS: 25 minutes”, not merely the unexplained number 25. The caption explains what collection of facts the table contains; it does not replace either header relationship.
(c) The code element's text is <em>focus</em>. It has no em descendant. The character references in the source are decoded into literal text characters as part of parsing; that resulting character sequence is not parsed a second time as markup creating an emphasis element. code also does not generally disable HTML parsing: the escaping is what expresses the literal angle brackets here.
Read < as a text less-than sign and > as a text greater-than sign. Those characters become part of a text node. By contrast, an unescaped <em>focus</em> in the HTML source would be markup for an element. When the purpose is to teach or display source syntax, escaping the brackets preserves the source as readable text; changing only the surrounding element to code would not accomplish that.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark each for the document-title, top-level-heading and main-content roles. |
| b | 3 | One mark for associating 25 with CSS and Minutes through the headers; one for the caption's whole-table role; one for resolving the link to the table's ID. |
| c | 2 | One mark for the exact literal text; one for explaining that no emphasis element is created because the escaped characters are text. |
Q03 — Derive a style and a responsive box (10 marks)
(a) The colour is blue at both stated viewport widths. The article matches all three colour selectors. Their specificity values are:
| Selector | Specificity |
|---|---|
.tile | (0,1,0) |
#board .tile | (1,1,0) |
.tile.notice | (0,2,0) |
The triple records counts in the order IDs, class-like selectors, element-like selectors. For these selectors, #board contributes one ID, and each .tile or .notice contributes one class. The descendant space expresses the relationship between the ancestor and article; it does not add another specificity count.
Compare triples from left to right, stopping at the first difference. They are not decimal numbers and their components are not added together. First compare (1,1,0) with (0,2,0): the first component is already larger, so the ID-containing selector wins without comparing the class count. Then (0,2,0) beats (0,1,0) at the second component. Under the stated common origin, importance and layer conditions, #board .tile therefore supplies blue. The later .tile.notice rule cannot win by source order because specificity is already different. The media query contains no colour declaration.
Use this method to explain why a matched rule lost: match the element, check that competing rules are being compared at the same cascade stage, compare specificity, then use source order if that comparison is tied. Reading only the last colour declaration would skip the stage that decides this example.
(b) At 600 CSS pixels, the media query does not match. The article uses content-box, so its content width is 240 pixels. Both 12-pixel paddings and both 3-pixel borders are outside that declared content width:
border-box width = 240 + 12 + 12 + 3 + 3 = 270 CSS pixels
Here px means CSS pixels, the unit used by the stylesheet and viewport condition. It does not necessarily count physical display pixels. The viewport's 600-pixel width decides which rules apply; the article's declared 240-pixel width describes a different box inside that viewport.
Draw one horizontal slice before doing arithmetic:
| From the outside left to the outside right | Left border | Left padding | Content | Right padding | Right border |
|---|---|---|---|---|---|
| Width at 600 CSS pixels | 3 | 12 | 240 | 12 | 3 |
The content takes 240 pixels. The two padding strips together take 24, and the two border strips together take 6. Add 240 + 24 + 6 to obtain the outer edge-to-edge border-box width 270. This calculation contains no margin because the question supplies no relevant margin. It also contains no extra 600: the viewport is a condition for choosing the sizing rule, not a second part of the article's width.
(c) At 800 CSS pixels, the media query matches and changes sizing to border-box. The declared 240 pixels now includes padding and borders. The border-box width is 240 pixels, and the content width is 240 - 24 - 6 = 210 pixels. Changing this sizing interpretation does not remove the padding or borders.
Work from outside to inside this time. Reserve the two 3-pixel borders from the total 240, leaving 234. Reserve the two 12-pixel paddings from that remainder, leaving 210 for content. The corresponding slice is 3 + 12 + 210 + 12 + 3 = 240. The mode changes which box the word width describes. It does not stretch the article to 800 pixels or change the numerical padding declarations.
(d) At 600 pixels, the default md threshold of 768 pixels has not been reached. Both columns use col-12, so each spans the row and the two stack. At 800 pixels, col-md-6 applies to each: they each span 6 of the row's 12 sizing units and fit alongside one another. These proportions refer to the grid row; they do not assert that a column is exactly half the whole viewport.
Compute the active fraction only after choosing the breakpoint: below md, each is 12/12 = 1 row width; from md, each is 6/12 = 1/2 row width. The enclosing container, row and gutters determine the actual region being divided. “Half a row” and “400 CSS pixels because the viewport is 800” are therefore different claims. The first follows from the classes; the second would require the row's actual width and layout details.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark for blue; one for the correct competing specificity values; one for applying specificity before source order under the stated conditions. |
| b | 3 | One mark for the 240-pixel content width; one for adding both paddings and borders; one for the 270-pixel border box. |
| c | 2 | One mark for the 240-pixel border box; one for the 210-pixel content width with the correct subtraction. |
| d | 2 | One mark for stacked full-row columns at 600; one for two half-row columns at 800 using the 768-pixel threshold. |
Q04 — Trace conversion and independent closures (10 marks)
["51",4,false]
5
3
4
(a) JSON.stringify makes the first line's value types explicit: "51" is a string, 4 is a number, and false is a Boolean. The remaining lines are the values returned by the three closure calls.
(b) Each call to makeCounter(2) creates a separate invocation with its own value binding. The returned arrow retains access to that invocation's binding:
| Operation | first's retained value | second's retained value | Return |
|---|---|---|---|
| Create both counters | 2 | 2 | — |
first(3) | 5 | 2 | 5 |
second(1) | 5 | 3 | 3 |
first(-1) | 4 | 3 | 4 |
The functions have the same definition and the same initial number, but they do not share one retained binding. Returning from makeCounter also does not erase the bindings still used by its returned function.
Think of each factory call as creating a separate numbered counter cell. Assigning its returned function to first or second gives access to the corresponding cell. To reproduce the result, label the cells before the calls, then update only the cell reached by the function actually invoked. This method distinguishes a closure's retained state from the new delta argument supplied on each call.
(c) "5" + 1 uses string concatenation and produces "51". Subtraction performs numeric conversion here, so "5" - 1 is the number 4. Strict equality keeps the number 0 distinct from the string "0", giving false; it does not convert them into one type before comparing.
The quotation marks matter at the input stage, even when two values look alike when displayed. If a form calculation intends numeric addition, convert the string deliberately before using +; otherwise the operator can build text instead. Predicting both the intermediate type and value is more reliable than remembering an output pattern for one pair of digits.
| Part | Marks | Award for |
|---|---|---|
| a | 4 | One mark for each of the four correct output lines. |
| b | 4 | One mark for separate bindings created by the two factory calls; one each for the state changes 2→5, 2→3 and 5→4. |
| c | 2 | One mark for explaining concatenation versus numeric subtraction; one for strict equality comparing the different types without conversion. |
Q05 — Separate an outer array from its elements (8 marks)
[2,2,3,25]
[5,7]
[{"value":"start","done":false},{"value":"finish","done":false},{"value":"done","done":true}]
(a) sessions and alias name the same outer array of length 2. copy is a different outer array; after the push its length is 3. The first nested object is shared, so its minutes is 25 when read through sessions.
The first output packs four different observations into one JSON array. In order, they are the number of entries in sessions, the number in alias, the number in copy, and the first session object's numeric minutes value. Array lengths count entries; they are not times. Thus [2,2,3,25] contains three counts followed by a 25-minute quantity.
(b) slice() copies the outer array's element references, not the nested objects themselves. Thus sessions[0] and copy[0] still refer to one object, and editing its property is visible through either reference. The push modifies the separate copy array's membership; it does not append an element to sessions. The pipeline starts from sessions, whose minutes are now 25 and 35. Both satisfy the filter, and dividing each by 5 produces [5,7]. The new 10-minute object is not part of this pipeline's input.
Use labels for the objects to keep the two levels separate. Call the original outer array A, the copied outer array B, and the initial session objects P and Q. These are diagram labels, not additional program variables.
| Name or array position | Refers to what? | Consequence |
|---|---|---|
sessions and alias | The same outer array A | They always read the same array length in this trace. |
copy | Another outer array B | Changing B's membership does not change A's membership. |
| A's first entry and B's first entry | The same object P | Updating P's minutes is visible through either array. |
| A's second entry and B's second entry | The same object Q | Q still has 35 minutes. |
| B's appended third entry | A new 10-minute object | A has no entry referring to that new object. |
Now follow the operations rather than treating “copy” as a promise of complete independence:
| After this operation | Length of A | Length of B | P's minutes |
|---|---|---|---|
copy = sessions.slice() | 2 | 2 | 20 |
copy[0].minutes = 25 | 2 | 2 | 25 |
copy.push(...) | 2 | 3 | 25 |
A shallow copy is useful when an independent list membership is enough. If independently editable nested records are required, copying only the outer array does not meet that requirement; the nested records being edited need their own ownership or copies.
For the pipeline, evaluate the predicate before the transformation. The 25-minute object passes 25 >= 25; the 35-minute object passes 35 >= 25. The division scales each retained duration by 1/5. If we interpret 5 as a five-minute unit, the results are 25 minutes / 5 minutes = 5 such units and 35 minutes / 5 minutes = 7 such units. JavaScript stores ordinary numbers here; this interpretation supplies the units. Calling the results “5 minutes and 7 minutes” would change that interpretation. The code does not round or partition sessions into whole groups: a retained duration of 26 minutes would give 26 / 5 = 5.2. A requirement to count only complete five-minute blocks would need an additional stated rule.
| Pipeline stage | Data shape | Values at that stage |
|---|---|---|
sessions | Array of objects, each with a numeric minutes property | Objects containing 25 and 35 minutes. |
After filter | Another array of the retained objects | Both original objects pass; filtering has not converted them into numbers or copied the objects. |
After map | Array of numbers | The callback returns the scaled durations 5 and 7. |
In words, the data changes from an array of objects to an array of retained objects, then to an array of numbers. The callback's return value decides the mapped element's shape. The filtering callback instead decides whether to keep an existing element. Choose these operations according to the required result: first select qualifying session records, then transform each retained duration into the required numeric scale.
If the requirement were to change only the copied list's first record, replace the original property assignment with copy[0] = { ...copy[0], minutes: 25 } in a fresh run. That expression creates another record and assigns its reference to B's first entry; it does not edit P. The original sessions[0].minutes would then remain 20. The later pipeline still reads sessions, so only the 35-minute record passes its threshold and its result becomes [7]. This changed result is a direct check that the edit belongs to the intended record, rather than only to a differently named reference to the same record.
(c) The first two next() calls stop at yield, so their done flags are false. The third reaches the generator's return "done", completing it and producing done: true. One additional call would return { value: undefined, done: true }. A completed generator remains completed; the final returned string is not another yielded item.
Keep the property name done separate from the string value "done". The property is a Boolean completion flag; the string happens to be the value chosen by this generator's final return. Each next() resumes the same suspended execution. Calling the generator function again would create a new cursor, whereas advancing the completed cursor does not restart it.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark for each exact output line, including the generator completion flags. |
| b | 3 | One mark for shared nested identity; one for independent outer-array membership; one for tracing 25 and 35 through the filter/map to 5 and 7. |
| c | 2 | One mark for distinguishing yield from return/completion; one for the additional result with undefined value and done true. |
Q06 — Follow nodes and a bubbling event (10 marks)
2,3,Changed
button:word:choose
bubble:word:panel
(a) The collection observation precedes the event because it is logged before the final .click(). The click then invokes the listener on the button before reaching the listener on its ancestor panel.
(b) The original querySelectorAll result records the two initial matching nodes, so its length remains 2 after the append. It still refers to those actual nodes: changing the first node's text is therefore visible through snapshot[0]. The live class collection follows document membership and includes the newly appended li, giving length 3. For the click, the originating span is inside the button, which is inside the panel. Ordinary bubbling reaches the button's listener before the panel's listener.
“Static” describes which nodes are members of this particular result, not whether those nodes can change. To redo the question, list the two recorded node references at query time; then append the third node to the document without adding it to that recorded list. Read the first recorded node's current text at the logging line. This yields a fixed membership count alongside a changed property value without any contradiction.
(c) In both lines, word is the originating element's ID, obtained from event.target. The listener's event.currentTarget is choose in the button listener and panel in the panel listener. Those listeners observe an event that began in a descendant; their registration elements need not be its originating element. Bubbling does not change the original target into each successive ancestor.
This distinction is useful when one ancestor listener handles clicks from several descendants. The current target tells us which listener is running; the original target tells us where this click began. Do not infer a second click from the two event output lines: they are two observations of the one dispatched click as it bubbles.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark for each exact output line in its correct position. |
| b | 4 | One mark each for static membership, mutable recorded nodes, live membership, and the button-before-panel bubbling explanation. |
| c | 3 | One mark for target word; one for currentTarget choose then panel; one for explaining that ancestor listeners can observe an event originating in a descendant. |
Q07 — Distinguish identity, data and a method receiver (6 marks)
[true,false,1,"undefined"]
Sketch
(a) alias receives the same object reference as original, so their strict identity comparison is true. Serializing and parsing constructs a new object, so original === roundTrip is false. That construction occurs while the original nested count is still 1. The subsequent alias mutation changes the original nested object to 2, but it does not change the separate parsed nested object, whose count remains 1.
The order of the copy and edit matters. First the data representation is created from the count 1. Parsing that representation constructs a new outer object and nested meta object. Only afterward does the alias change the original nested count to 2. Unlike Q05's outer-array slice, this particular JSON round trip reconstructs the represented nested data rather than retaining the same nested object reference.
(b) The method is a function-valued property. JSON serialization of this object does not include that method, and parsing the JSON does not recreate a function. Reading the absent roundTrip.describe gives undefined, whose reported type is the string "undefined". In original.describe(), the method call's receiver is original, so this.name reads its name property, "Sketch". The method body has not been called with roundTrip as its receiver.
Use JSON when the required result is the supported data representation, for example when exchanging a name and count. Do not choose it on the assumption that every aspect of an arbitrary object, including its behaviour, will survive. For a method result, locate the object to the left of the dot at the actual call; that receiver explains this question's this value.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark for both correct output lines; one for the alias/new-object identity distinction; one for count 1 being copied before the later mutation. |
| b | 3 | One mark for JSON omitting the function-valued method; one for absent property producing typeof undefined; one for the original method receiver giving Sketch. |
Q08 — Inspect entries and persistent validity state (9 marks)
[["title","CSS"],["pages","3"],["include","yes"],["format","notes"]]
[true,false]
[true,false]
[false,true]
(a) Entries follow the successful controls in document order. title contributes "CSS"; the number control contributes the string "3"; the checked checkbox contributes include=yes; and the selected option contributes format=notes. The unchecked skip control contributes nothing. The disabled hint control contributes nothing. No submitter was supplied to this FormData call, so the submit button's action=save entry is absent as well.
Read the first line by following each data transformation. new FormData(form) constructs a FormData object containing the successful entries. Spreading that iterable produces an ordinary outer array with four entries. Each entry is itself a two-element array: first the name, then its value. In this question all eight members of those four pairs are strings, giving a 4 × 2 entry structure. For example, the second pair is ["pages","3"]: its first string names the field and its second string contains the field's value. JSON.stringify then converts that array structure into one JSON text string, which is what the console prints. The printed brackets describe the array and pairs; they do not make each pair a separate JSON object.
The names come from name, not from the visible labels or IDs. Number-control values are still represented as strings in these entries. To calculate a page total, convert the extracted pages string deliberately to a number, or read this number input's valueAsNumber, then check the intended numeric rule. For the initial value, Number("3") gives the number 3. Adding numeric 2 after conversion gives 5, whereas applying + directly to the string "3" and number 2 would produce "32". Entry construction, array expansion, JSON serialization, numeric conversion and validation perform different jobs; identify the value at the stage you are actually using.
(b) With minimum 1 and step 1, the valid numeric step values are 1, 2, 3 and so on. Setting pages.value to "1.5" gives a step mismatch and makes the form invalid, producing [true,false].
The next call records a nonempty custom validity message. Changing the value to "4" fixes the numeric step mismatch but does not clear that custom error. Thus customError is true and the form is still invalid: [true,false].
Finally, setting the custom message to the empty string clears the custom error. The current value 4 now satisfies the required, minimum and step constraints; the other controls also satisfy their applicable constraints. The result is [false,true]. A corrected value and a cleared custom error are two distinct state changes.
Trace those states explicitly. “Form valid?” below means the result of checking all applicable constraints at that stage, with the other controls unchanged.
| Stage | Current text in pages | stepMismatch | customError | Form valid? | Observation |
|---|---|---|---|---|---|
| Initial entries are constructed | "3" | false | false | true | The entry stores the string "3". |
Assign "1.5" | "1.5" | true | false | false | The first validity pair reads the step flag and form result. |
Set the custom message, then assign "4" | "4" | false | true | false | The second pair reads the custom flag and form result. |
| Clear the custom message | "4" | false | false | true | The final pair confirms that both obstacles are cleared. |
The step is measured from the step base, here the minimum 1. With step 1, the step grid is 1 + n for an integer step count n. The separate minimum constraint also requires the value to be at least 1, so an allowed value here has n >= 0. The difference 1.5 - 1 = 0.5 is not an integer number of steps; 4 - 1 = 3 is. That explains the browser's step flag before considering any custom rule. A nonempty custom message then supplies an additional reason for invalidity. Replacing the text in the field does not automatically erase a message that application code set earlier.
When correcting such a form, inspect which validity flag remains true. If the numeric value violates the step rule, correct the value or the rule as appropriate. If the remaining problem is an obsolete custom message, clear it when re-evaluating the relevant user input. Repeating checkValidity() only observes the current state; it does not perform either repair.
(c) An enabled, named, read-only text control would contribute its current value, here hint=draft. Read-only controls and disabled controls have different submission behaviour. Read-only restricts this interface; it does not prove that every request reaching a service originated from this interface or followed its constraints. The receiving service must validate the values it accepts.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark for the exact ordered entries with pages as a string; one for the unchecked/disabled omissions; one for excluding the button when no submitter was supplied. |
| b | 4 | One mark for the three exact validity lines; one for the numeric step mismatch; one for the persistent custom error; one for clearing it and obtaining a valid current value. |
| c | 2 | One mark for the enabled read-only entry; one for explaining why receiving-side validation remains necessary. |
Q09 — Locate the failed asynchronous step (8 marks)
fulfilled:503:false
later
json-error
request-error
(a) The first awaited promise fulfils with the constructed response, which has status 503 and ok === false. Its body still contains valid JSON with the message later. The two separate catches then log their respective failure stages.
Follow the held value through the first two lines of processing. new Response(...) first creates a response object with status, headers and a body. Promise.resolve(...) wraps that object in an already fulfilled promise. Awaiting that promise gives the Response object to response; it does not turn its body into a JavaScript object. Calling response.json() starts a separate body-reading/parsing operation and returns another promise. Awaiting that second promise gives the parsed object, whose message property is then read.
The type-and-shape path is therefore Promise<Response> → Response → Promise<parsed data> → { message: "later" } → the string "later". Each arrow is a particular operation in the program, not an automatic conversion performed by the previous arrow. This separation is useful because a response status and the data interpreted from its body can describe different kinds of failure.
(b) A fulfilled promise for a response and a successful HTTP status are different observations. Promise.resolve in this example fulfils with a Response; its 503 status does not turn that promise into a rejection. ok is false because 503 is outside the success range 200–299.
The response's error status does not prevent its valid body from being read and parsed. The JSON object contains message: "later", so the second line is later.
For the next response, the status is 200 but the body text {broken is not valid JSON. The rejected operation is the promise returned by .json(), caught by its surrounding try/catch. In the final block, Promise.reject(...) represents an operation that failed before returning a usable response; awaiting it throws into the final catch.
Use four questions to choose the next action:
| Layer | What is held or checked? | Observation in this example | Appropriate next decision |
|---|---|---|---|
| Response availability | A promise that should yield a response | The final explicitly rejected promise yields no response. | Handle that rejection; there is no response status or body from that operation to inspect. |
| HTTP status | The obtained response's status and ok | The first response exists, but status 503 makes ok false. | Follow the unsuccessful-status branch; read the documented error representation if it is needed. |
| Body representation | The body-reading promise and its parsed result | The 503 body is valid JSON; the separate 200 body rejects JSON parsing. | Use parsed data only after parsing succeeds; a successful status cannot repair malformed JSON. |
| Application meaning | The parsed object's fields and the operation's contract | The first object supplies the message later; it supplies no completed-storage result. | Interpret those fields together with the unsuccessful status before deciding which user-visible result to show. |
For example, a user interface should not report a completed save merely because the first body is parseable. It has obtained an error response and an explanatory message. Conversely, when JSON parsing fails on the 200 response, manufacturing a normal result from an empty object would hide the failed representation step. Locating the failed layer tells us whether to inspect a rejection, an HTTP result, a malformed representation or a well-formed application's answer. The constructed responses and rejected promise make these distinctions observable in memory; they do not establish the behaviour of a particular remote service.
| Part | Marks | Award for |
|---|---|---|
| a | 4 | One mark for each exact line in order. |
| b | 4 | One mark each for response availability versus status, readable JSON despite 503, body-parse rejection despite 200, and the final pre-response rejection stage. |
Q10 — Separate the current task from its continuations (6 marks)
A
C
E
B
D
(a) The synchronous sequence is A, C, E. The queued promise continuations then produce B followed by D.
(b) Registering the .then() callback schedules its later continuation; it does not run the callback in the middle of the current task. Calling study() immediately enters that async function and executes the code before its first await, so C is logged before execution returns to the outer script and logs E.
Even though the awaited promise is already fulfilled, the function's code after await resumes later. The current task therefore reaches E before either continuation logs. The callback producing B was queued by the earlier .then() registration before the await continuation producing D was queued. Under the stated script and no other scheduled work, they run in that order. Merely declaring a function async does not defer its entire body or pause the whole outer script.
Here the queue column lists only the promise continuations relevant to this script. Read its leftmost item as the next one to run after the current task finishes.
| Currently executing step | Pending continuations, in order | Printed so far |
|---|---|---|
| Execute the first log | None | A |
Register the .then callback | B callback | A |
Call study() and execute its first log | B callback | A C |
Reach await Promise.resolve() and pause that function's continuation | B callback, resume study at D | A C |
| Return to the outer script and execute its last log | B callback, resume study at D | A C E |
| Finish the current task; run the first queued continuation | Resume study at D | A C E B |
| Resume the async function after its await | None | A C E B D |
This method prevents two common mistaken predictions. The word async does not move C into the queue: that instruction is before the first await and runs during the call. An already fulfilled awaited promise also does not place D back into the current synchronous segment: the continuation still resumes later. Record what is executing, what is waiting and what has already been printed separately, rather than reading the source as one uninterrupted top-to-bottom list of logs.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark for A first, one for the synchronous C/E order, and one for B/D following in that order. |
| b | 3 | One mark for execution before the first await; one for finishing the current task before its continuations; one for the earlier registration queuing B before D. |
Q11 — Process the queue rather than count setter calls (10 marks)
1
2
3
(a) The fresh component displays 1. After its first committed click it displays 2; after its second committed click it displays 3. Each click uses a handler from the current committed render.
There are three different things to track. The snapshot is the count supplied to the component function for one render. The pending value is the result being calculated while React processes queued state operations. The committed view is the text currently shown in the button. Calling the setter requests work on the retained state; it does not assign a new value to the current function call's const count.
(b) On the first click, the handler's captured count is 1 throughout the handler. Both occurrences of count + 1 therefore compute the replacement value 2. The queue is processed in order:
| Queued operation | count captured by this handler | Pending value afterward |
|---|---|---|
Replace with captured count + 1, which is 2 | 1 | 2 |
Apply value => value * 3 to pending 2 | 1 | 6 |
Replace with captured count + 1, which is still 2 | 1 | 2 |
The updater's value is the preceding pending value, not necessarily the handler's captured count. Its multiplication does run, but the final replacement overwrites that result. On the second click, the new captured count is 2: the queue goes to 3, then 9, then back to 3. Three calls are three ordered queue operations, not three implicit increments.
Substitute values at each step. The first call has already calculated the ordinary number 1 + 1 = 2 before it queues a replacement. The second call supplies a function, so React can later apply it to pending 2 and obtain 2 × 3 = 6. The final call again calculates from the handler's unchanged snapshot: 1 + 1 = 2. It therefore replaces 6 with 2, rather than adding one to 6. React renders using the final state 2 and commits the resulting button text. The temporary pending value 6 need not appear on screen between those operations.
That new render is another call to the component function, with a new local count binding containing 2 and a handler that can read that binding. After its commit, the next click uses that new handler. This explains why the second click starts from 2 rather than reusing the initial handler's 1 forever.
This behaviour agrees with the ordinary closure in Q04. There, a returned function directly changes its retained let value binding with +=, so the next call reads the changed binding. Here the handler reads the const count of its particular render, and setCount does not reassign that binding. React retains the component's mounted identity and state across calls, then supplies the next state to another render call. The useful rule is to identify which binding is retained or newly created and which operation changes it; a closure does not universally freeze every value it can read.
(c) Use updater functions for the two additions:
setCount(value => value + 1);
setCount(value => value * 3);
setCount(value => value + 1);
On a fresh count of 1, the pending values become 2, 6 and 7, so the committed display is 7. The requested arithmetic order is retained while each operation now receives the preceding pending result. Another click would yield 8, 24 and 25.
Use an updater when the next operation is defined in terms of the preceding pending state. Use a replacement when a particular already calculated value is the intended next state. In the repaired version, the last addition means “add one to the preceding result 6”, producing 7. In the original, it means “replace the state with the number 2 calculated from this render”, producing 2. Choosing the form expresses the dependency that makes those answers different.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark for each recorded text: initial 1, first click 2, second click 3. |
| b | 4 | One mark for the captured count remaining 1; one for pending 2→6→2; one for the updater receiving the pending value; one for explaining the final replacement overwriting multiplication rather than counting three increments. |
| c | 3 | One mark for each addition rewritten as an updater; one for the first repaired result 7 with the correct arithmetic order. |
Q12 — Retain item identity and balance lifecycle work (8 marks)
initial:a=false,b=false
marked:a=true,b=false
reordered:b=false,a=true
active:a,b
after-unmount:0
(a) Both fresh cards begin unpinned. Clicking a updates only that card's state. Reordering changes the DOM order to b,a while retaining the pin on a. The two registered IDs are sorted for the set observation, and both registrations are removed by the final cleanup.
Start by separating three observations: which item is displayed at a position, which mounted card retains a pinned value, and which item ID has an active Effect registration. A change of position need not change the identity of the mounted card.
| Completed stage | DOM order | Pinned state retained by card a | Pinned state retained by card b | Registered IDs |
|---|---|---|---|---|
| Fresh mount and Effect setup | a,b | false | false | a,b |
Click a and commit | a,b | true | false | a,b |
| Reorder the keyed items and commit | b,a | true | false | a,b |
| Unmount and finish cleanup | No cards | Its mounted state is removed | Its mounted state is removed | Empty |
The observation lines list cards in DOM order, so after reordering they must start with b=false. The set line uses sorted IDs, independently of DOM order, so it is still active:a,b.
(b) Each SessionCard owns its own pinned state. The list supplies each card's stable item ID as its key. When the parent reorders the items, React can match the same SessionCard instances by those keys. The instance for a keeps its true state, even though it is now second; b remains false. A key based on the array index would instead identify positions. With position keys after this reorder, state previously associated with the first card could become associated with item b, which is now at that position.
Here an instance means the mounted component identity React retains in this tree, identified by its component type and place/key relationship. It is not a claim that the function's local variables survive by being the same function call. React calls the component function again when it renders. useState supplies the state retained for the matched mounted card, and that call creates a new local snapshot and handler. The pin belongs to card a's retained state, not to the numerical fact that it happened to be displayed first.
The index-key variation shows when the method matters. Initially index key 0 belongs to the mounted card showing a, whose pin becomes true. After reordering with index keys, the mounted identity at key 0 shows b instead and can retain that true pin; the next position shows a with the other false state. That gives b=true,a=false, unlike the stable-ID result. When state is meant to follow an item through a reorder, use the item's stable identity as the key.
(c) Each mounted card's Effect registers its item ID. Reordering preserves the keyed instances, and their individual item.id dependencies do not change. It therefore does not require an extra registration or cleanup for either card: the set contains a and b. When the root unmounts, each card's cleanup deletes its ID. After both cleanups finish, the set is empty and its size is 0. This observation shows that the registrations and cleanups balance.
The timing matters. Rendering describes a list element and button; Effect setup runs after the commit to synchronise the registration with the mounted card. The question waits for those Effects before reading the set, so both IDs are present at its initial observation. Clicking a button changes pinned, but the Effect depends on item.id, not pinned; its dependency is unchanged. Reordering also leaves each keyed card with the same ID, so there is no dependency change requiring the old setup to be replaced.
The cleanup function belongs to that setup. For card a, setup adds a and its cleanup removes a; the same pairing holds for b. Unmounting ends both synchronisations and invokes those cleanups. Neither ID changes in this question, and the final unmount leaves the set with size 0. Do not reset a pin merely because its function renders again, or create another registration merely because an item moves to another position.
| Part | Marks | Award for |
|---|---|---|
| a | 3 | One mark for initial and marked observations; one for the reordered observation retaining a=true; one for both active and post-unmount observations. |
| b | 3 | One mark for child-owned state; one for stable IDs retaining the intended instances; one for explaining that index keys identify positions instead. |
| c | 2 | One mark for unchanged item-ID dependencies retaining two registrations; one for both unmount cleanups leaving an empty set. |
Use the marks to choose the next revision step
The allocation separates outcomes from explanations. A correct output guessed for the wrong reason earns its output mark but does not establish the corresponding reasoning point. Conversely, a small transcription error need not erase a correctly justified independent explanation. Use the question-level result to identify which model needs repair, then test a changed input rather than memorising this answer sequence.
The total is 100. Use it alongside the separate question results to choose the specific topic and reasoning step to revise next.