Blog/A JavaScript Exercise Should Test a Decision

A JavaScript Exercise Should Test a Decision

A JavaScript exercise should test a decision. Not a keystroke. Not whether the learner can copy the method name from the previous paragraph. A decision.

That decision can be small: let or const, map() or filter(), event.target or a saved element reference, success path or error path, mutate or copy. But it has to exist.

If the prompt removes every choice, the learner may still produce valid code. The problem is that valid code is not the same thing as learning. A typing task can pass without forcing the learner to build the mental model they will need five minutes later.

Good exercises leave one meaningful choice unspoken.

The Weak Version Names The Move

Weak beginner exercises often sound tidy:

Use map() to create an array of product names.
Use addEventListener() to update the message.
Use try/catch to handle the fetch error.
Use destructuring to get name and email.

These can be useful as the first contact with a new syntax. At some point a learner does need to see the shape of addEventListener() or a destructuring pattern.

But if every exercise names the tool, the learner is not practicing judgment. They are matching a label to a slot.

The stronger version describes the behavior and lets the learner choose:

Return the visible labels for products that are in stock.
Change the status only after the save button is clicked.
Load a user, but make sure a 404 does not enter the success path.
Return a public profile without leaking private fields.

Now the learner has work to do before writing code. They have to ask:

  1. What shape should come out?
  2. What state is allowed to change?
  3. Where is the boundary between success and failure?
  4. Which values should be kept, omitted, copied, or normalized?

That is the exercise.

Higher-Order Functions Test Output Shape

The existing array-methods post argues that map(), filter(), find(), and reduce() are judgment tools, not vocabulary words. That same idea should drive the exercises.

This is thin:

Use filter() to get paid orders.

The decision has already been made. The learner can complete the task by remembering where the predicate goes.

This is better:

Return the email addresses for users who have verified their accounts.

Now there are two shape decisions:

  1. Keep only verified users.
  2. Transform each remaining user into an email string.

The code may end up as a filter().map() chain, but the prompt did not hand that chain over preassembled:

function verifiedEmails(users) {
  return users
    .filter((user) => user.verified)
    .map((user) => user.email);
}

The tests should protect the decision too. A test that only checks one lucky output can miss the wrong model. A better set checks that unverified users disappear, the returned values are strings, and the original records are not required in the final shape.

That is also why chain-order exercises matter. If a learner maps lessons to titles too early, they throw away published and archived before the filter can use them. The mistake is not "bad syntax." The mistake is choosing the final shape before the selection work is done.

The map vs filter vs find resource explains the contracts. An exercise should make the learner choose the contract under light pressure.

DOM Exercises Test Timing And State

DOM exercises get weak when they turn into selector transcription:

Select #save-status and set its textContent to "Draft saved".

That tests whether the learner can touch the page. Fine once. Not enough for event-driven code.

A better exercise makes timing part of the contract:

When the save button is clicked, update the status text to show that the draft is saved.

Now the learner has to decide where the update belongs. If they run the handler during setup, the page changes too early. If they pass a callback to the listener, the browser runs it when the click happens.

The test should make that distinction visible:

const saveButton = document.querySelector("#save-draft");
const saveStatus = document.querySelector("#save-status");

saveButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));

console.log(saveStatus.textContent);
// "Draft saved"

For counters and live previews, the decision gets richer. Where does state live between clicks? Should the handler read the current input value or reuse the starting value? Should blank text leave stale UI in place or show an empty-state label?

Those are product decisions disguised as beginner JavaScript. Good. That is where the lesson becomes real.

The event target and currentTarget resource is useful because many DOM bugs are really target decisions. Which element received the listener? Which element started the event? Which element should change? An exercise that asks the learner to choose the target teaches more than one that only asks them to paste a selector.

Async Exercises Test The Error Boundary

Async practice often over-focuses on syntax:

Use async and await to fetch the user.

That is not useless, but it misses the decision that breaks real apps: which path is success, and which path is failure?

With fetch(), the naive model is especially tempting:

async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

This looks done. It is not done.

A 404 response does not make fetch() reject. The server answered, so the Promise resolves with a Response. If the exercise only tests the happy path, the learner can accidentally send an error body into the success path and still pass.

A stronger exercise says:

Load a user profile. If the server returns a non-2xx response, throw before parsing the success data.

Now the learner has to separate transport failure from HTTP failure:

async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    throw new Error(`Could not load user ${id}: HTTP ${response.status}`);
  }

  return response.json();
}

The important decision is not "use try/catch." Sometimes the function should throw and let the caller decide how to render the failure. Sometimes the function should catch, log, and rethrow. Sometimes the UI should return an error state. The exercise has to name the contract clearly enough that one of those choices is correct.

The fetch error handling resource explains the boundary. A good exercise tests whether the learner can enforce it.

Object Exercises Test Shape And Ownership

Objects are another place where prompts can accidentally remove the real work.

This is mostly syntax recall:

Use destructuring to get passwordHash and internalNotes from user.

This is a decision:

Return a public profile that removes passwordHash and internalNotes, preserves every other property, and does not mutate the original user.

Now object rest is not just punctuation. It is a strategy for keeping unknown public fields without listing them by hand:

function publicProfile(user) {
  const { passwordHash, internalNotes, ...publicFields } = user;
  return publicFields;
}

The tests should catch the shortcuts:

ShortcutWhy it is wrongTest that catches it
Return { id, name } onlyDrops unexpected public fieldsAdd timezone or handle and assert it survives
Delete fields from userMutates caller-owned dataAssert the original object still has its private fields
Return the original userLeaks private fieldsAssert passwordHash is not on the result

The same principle applies to Object.keys(), Object.values(), and Object.entries(). The good question is not "Can you call Object.entries()?" The good question is "Do you need keys, values, or key-value pairs?"

That is why an exercise that builds a query string from an object should test valid falsy values:

toQueryString({
  q: "js",
  archived: false,
  page: 0,
  empty: null,
});

If the learner filters with Boolean(value), they accidentally erase false and 0. The issue is not that they forgot a method. The issue is that they chose the wrong boundary for "missing." null and undefined should disappear. Valid falsy values should remain.

The spread operator guide covers the copy side of this decision. The exercise should make the data-shape consequence unavoidable.

The Test Should Punish The Likely Wrong Decision

An exercise is not finished when the prompt sounds good. The tests have to catch the mistake a learner is most likely to make.

If the likely mistake is using forEach() instead of returning a transformed array, assert the return value.

If the likely mistake is updating a DOM variable but not the page, inspect the DOM.

If the likely mistake is assuming fetch() rejects on 404, mock a 404 and assert the error path.

If the likely mistake is mutating nested state while pretending to copy it, assert that the original object and the nested object are still unchanged.

This is the difference between testing the prose and testing the contract. A prompt can say "do not mutate the original," but the test has to prove it. A prompt can say "only after the click," but the test has to check the before and after states.

Good tests are not mean. They are specific. They point directly at the decision the learner needed to make.

A Useful Prompt Rule

When writing a JavaScript exercise, try this rule:

Name the behavior. Hide the tool just enough.

Not forever. Not for a learner seeing a concept for the first time. But once the syntax has appeared, the prompt should move from "use this" to "make this true."

Instead of:

Use reduce() to count statuses.

Try:

Return an object whose keys are ticket statuses and whose values are counts.

Instead of:

Use the input event.

Try:

Keep the preview synced as the user edits the field, and show a fallback when the field is blank.

Instead of:

Use destructuring defaults.

Try:

Normalize the options object so missing values get defaults, but explicit false and empty strings are preserved.

The second version in each pair still has a right answer. It is not vague. It just asks the learner to find the tool by understanding the job.

That is the kind of pressure an exercise should create.

Key Takeaways

  • A strong JavaScript exercise leaves one meaningful decision for the learner to make.
  • Method-choice exercises should start from result shape, not method names.
  • DOM exercises should test timing, event targets, visible state, and repeated interactions.
  • Async exercises should separate success data, HTTP failures, and transport failures.
  • Object and destructuring exercises should test shape, ownership, omitted fields, and valid falsy values.
Share this post: