Skip to main content
ExamExplained
NSW · Software Engineering
Software Engineering study scene
§-Syllabus dot point
NSWSoftware EngineeringSyllabus dot point

Inquiry Question 2: How can data be better visualised using a web browser?

Use JavaScript in the browser to manipulate the DOM, handle events and make asynchronous requests

A focused answer to the HSC Software Engineering Module 2 dot point on client-side JavaScript. DOM manipulation, event handlers, fetch and async/await, the worked example, and the traps markers look for.

Reviewed by: AI editorial process; not yet individually human-reviewed

Have a quick question? Jump to the Q&A page

What this dot point is asking

NESA wants you to use JavaScript in the browser to read and modify the DOM, respond to user events, and make asynchronous HTTP requests with fetch. You need to write working code under exam conditions.

The answer

Selecting elements (DOM access)

The DOM (Document Object Model) is a tree representation of the HTML document. JavaScript reads and modifies it through methods like:

const heading = document.querySelector("h1");
const buttons = document.querySelectorAll("button.primary");
const username = document.getElementById("username");

Modifying the DOM

Common operations:

heading.textContent = "Welcome";
heading.classList.add("highlighted");
heading.style.color = "blue";

const newItem = document.createElement("li");
newItem.textContent = "New point";
document.querySelector("ul").appendChild(newItem);

document.querySelector(".old").remove();

Use textContent when inserting user-controlled strings. innerHTML parses and executes HTML, which can introduce XSS.

Events

JavaScript responds to user actions through event listeners:

const button = document.querySelector("#save");

button.addEventListener("click", (event) => {
  console.log("Saved at", event.timeStamp);
});

const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
  event.preventDefault();
  // ... custom handling, e.g. validation ...
});

Common events: click, submit, input, change, keydown, mouseover, load.

Asynchronous requests

Browser JavaScript exchanges data with the server through the fetch API, which returns a Promise that resolves with the response. Combined with async and await, this lets you write asynchronous code that reads like synchronous code, with try/catch for error handling.

async function loadUsers() {
  const response = await fetch("/api/users", {
    headers: { "Accept": "application/json" },
  });
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

// Use it:
loadUsers()
  .then(users => console.log(users))
  .catch(err => console.error(err));

An owned schematic shows what actually happens on the timeline when fetch() is called inside an async function: the call returns a Promise immediately, the main thread keeps running, and only code after await (or inside .then()) resumes once the network round trip finishes.

The fetch request lifecycle on the browser main thread A timeline diagram showing the browser main thread calling fetch, which immediately returns a pending Promise while the main thread continues running other code. Meanwhile a request travels across the network to the server, the server processes it and sends a response back, and only then does the awaited code resume on the main thread with the parsed JSON, updating the DOM. Main thread runs synchronous code, then keeps running while a Promise is pending fetch() called returns pending Promise await resumes here DOM updated with data Request Server processes and responds Response travels the network travels back Code written directly after fetch() without await runs immediately, before the response exists. Only await/.then() code is guaranteed to run after the response resolves.

Putting it together

A complete example - a search-as-you-type input:

<input id="search" placeholder="Search...">
<ul id="results"></ul>
const input = document.getElementById("search");
const results = document.getElementById("results");

let timeout = null;
input.addEventListener("input", () => {
  clearTimeout(timeout);
  timeout = setTimeout(async () => {
    const q = encodeURIComponent(input.value);
    const response = await fetch(`/api/search?q=${q}`);
    const items = await response.json();
    results.innerHTML = "";
    for (const item of items) {
      const li = document.createElement("li");
      li.textContent = item.title;
      results.appendChild(li);
    }
  }, 300);
});

The 300 ms debounce prevents a request on every keystroke. encodeURIComponent safely encodes user input for use in a URL.

Variables, types, control flow

JavaScript essentials:

const greeting = "Hello";       // immutable binding
let counter = 0;                // mutable binding
counter += 1;

const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2);    // [2, 4, 6, 8]
const even = numbers.filter(n => n % 2 === 0);  // [2, 4]
const sum = numbers.reduce((acc, n) => acc + n, 0);  // 10

function greet(name) {
  if (!name) return "Hello, stranger";
  return `Hello, ${name}`;
}

Security: never inject untrusted HTML

Setting innerHTML with a value that came from user input is one of the most common ways an XSS vulnerability slips into a front-end. The browser parses the assigned string as HTML, so any script tag, event handler attribute, or javascript URL inside it can execute. Use textContent or the DOM API instead.

// BAD - XSS risk
container.innerHTML = `<p>${userInput}</p>`;

// GOOD
const p = document.createElement("p");
p.textContent = userInput;
container.appendChild(p);

Exam-style practice questions

Practice questions written in the style of NESA exam questions on this dot point, with worked answer explainers. The year tag is the paper they imitate, not the source.

2025 HSC5 marksWrite JavaScript that, when a button is clicked, fetches a list of users from /api/users and displays each user's name in an unordered list.
Show worked answer →
<button id="load">Load users</button>
<ul id="users"></ul>
const button = document.getElementById("load");
const list = document.getElementById("users");

button.addEventListener("click", async () => {
  try {
    const response = await fetch("/api/users");
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const users = await response.json();
    list.innerHTML = "";
    for (const user of users) {
      const item = document.createElement("li");
      item.textContent = user.name;
      list.appendChild(item);
    }
  } catch (err) {
    list.innerHTML = `<li>Error: ${err.message}</li>`;
  }
});

Walkthrough: select the button and the list with getElementById, attach a click handler with addEventListener, fetch the JSON over HTTP, and rebuild the list. await pauses inside the async function until the response and JSON parse complete. Setting textContent (not innerHTML) prevents XSS if a user name happens to contain HTML.

Markers reward a real event listener (not inline onclick), fetch with await, error handling for non-2xx responses, and use of textContent rather than innerHTML for user-controlled data.

Practice questions

Original practice questions graded from foundation to exam level, each with a full worked solution. Try them before revealing the solution.

foundation2 marksWrite a single line of JavaScript that selects the element with id "total" and sets its visible text to "Done".
Show worked solution →
document.getElementById("total").textContent = "Done";

Marking criteria: 1 mark for correctly selecting the element by id, 1 mark for using textContent (not innerHTML) to set the text safely.

foundation3 marksA checkbox with id "agree" should enable a button with id "submit" only when it is checked. Write the JavaScript event handler that achieves this.
Show worked solution →
const agree = document.getElementById("agree");
const submit = document.getElementById("submit");

agree.addEventListener("change", () => {
  submit.disabled = !agree.checked;
});

Marking criteria: 1 mark for selecting both elements, 1 mark for listening to the change event, 1 mark for correctly toggling disabled based on agree.checked.

core4 marksThe diagram below shows the fetch request lifecycle used in this page's figure. Using the diagram, explain why code placed immediately after a `fetch()` call (without `await`) can run before the response arrives.
Show worked solution →

fetch() returns a Promise immediately and hands control back to the calling code before the network request completes, exactly as the diagram shows the Promise sitting in a "pending" state while the request travels to the server and back. Because JavaScript in the browser is single-threaded and non-blocking, any code written directly after the fetch() call (without await or a .then() callback) executes straight away, using the still-unresolved Promise object rather than the eventual response data. Only code inside an await expression (inside an async function) or inside a .then() callback attached to the Promise is guaranteed to run after the response has arrived and been parsed.

Marking criteria: 1 mark for stating that fetch() returns a Promise immediately, 1 mark for linking this to the diagram's pending state, 1 mark for explaining that the main thread continues executing subsequent code without waiting, 1 mark for correctly stating that only await/.then() code runs after resolution.

core4 marksExplain why the following code is vulnerable, and rewrite it to be safe: `document.getElementById("greeting").innerHTML = "Hi " + name;` where `name` comes from a URL query parameter.
Show worked solution →

The code is vulnerable to cross-site scripting (XSS). Because name is taken directly from a URL query parameter, an attacker can craft a link where name contains an HTML payload such as <img src=x onerror=alert(1)>. Assigning this string to innerHTML causes the browser to parse it as HTML rather than plain text, so the injected onerror handler executes in the victim's browser session, potentially stealing cookies or session tokens.

Safe rewrite:

document.getElementById("greeting").textContent = "Hi " + name;

textContent inserts the string as literal text; any HTML-looking characters are displayed as plain characters, not parsed as markup, which removes the injection vector entirely.

Marking criteria: 1 mark for naming the vulnerability (XSS), 1 mark for explaining how untrusted input flows into innerHTML, 1 mark for a correct working fix using textContent, 1 mark for explaining why the fix closes the vulnerability.

core5 marksData table: a page loads user counts from three endpoints in sequence using `await`, taking 220 ms, 180 ms and 260 ms respectively. Explain, with reference to `Promise.all`, how the total wait time could be reduced, and calculate the improved time assuming the requests can run concurrently.
Show worked solution →

Awaiting each fetch() one after another means the browser waits for each request to fully complete before starting the next, so the total time is the sum of the three: 220+180+260=660220 + 180 + 260 = 660 ms.

If the three requests do not depend on each other's results, they can be started at the same time and combined with Promise.all, which waits for all of them to settle concurrently rather than sequentially:

const [a, b, c] = await Promise.all([
  fetch("/api/counts/a").then(r => r.json()),
  fetch("/api/counts/b").then(r => r.json()),
  fetch("/api/counts/c").then(r => r.json()),
]);

Because the requests overlap, the total wait time becomes the time of the SLOWEST request, not the sum: max(220,180,260)=260\max(220, 180, 260) = 260 ms, a saving of 660260=400660 - 260 = 400 ms.

Marking criteria: 1 mark for identifying that sequential await calls sum their times (660 ms), 1 mark for correctly using Promise.all in code, 1 mark for explaining that concurrent requests are bounded by the slowest one, 1 mark for the correct concurrent time (260 ms), 1 mark for the correct time saved (400 ms).

exam6 marksA junior developer's search-as-you-type feature fires a fetch request on every keystroke and updates the results list directly with `innerHTML` from the server's response text. Evaluate this implementation and rewrite it to address the issues, explaining each change.
Show worked solution →

Issues to evaluate.

  1. No debouncing. Firing a request on every keystroke floods the server and wastes bandwidth; a user typing "laptop" fires six requests when only the last is useful.
  2. Race condition. Because requests can resolve out of order, an earlier keystroke's slower response can arrive after a later one and overwrite the newer, more relevant results.
  3. XSS via innerHTML. Rendering server text with innerHTML executes any HTML/script content embedded in it, which is dangerous if the search results ever echo back user-supplied text (e.g. "no results for <script>").

Rewritten implementation.

const input = document.getElementById("search");
const results = document.getElementById("results");
let timeout = null;
let requestId = 0;

input.addEventListener("input", () => {
  clearTimeout(timeout);
  const thisRequest = ++requestId;
  timeout = setTimeout(async () => {
    const q = encodeURIComponent(input.value);
    const response = await fetch(`/api/search?q=${q}`);
    const items = await response.json();
    if (thisRequest !== requestId) return; // stale response, discard
    results.innerHTML = "";
    for (const item of items) {
      const li = document.createElement("li");
      li.textContent = item.title;
      results.appendChild(li);
    }
  }, 300);
});

Why each change fixes an issue. The 300 ms setTimeout debounce collapses rapid keystrokes into a single request once typing pauses. The requestId counter tags each request and discards any response that is not the most recent, eliminating the race condition. Building list items with createElement and textContent instead of innerHTML renders result text safely regardless of its content.

Marking criteria: 1 mark for identifying the missing debounce, 1 mark for identifying the race condition, 1 mark for identifying the XSS risk, 1 mark for a correct debounce fix, 1 mark for a correct stale-response guard, 1 mark for a correct textContent/createElement fix.

ExamExplained