Salesforce JavaScript Developer Certification Study Guide (2026): Formerly JavaScript Developer I

Salesforce Certified JavaScript Developer 2026 study guide, formerly JavaScript Developer I

This is a deep study guide for the Salesforce Certified JavaScript Developer exam (exam code JS-Dev-101), the credential that was called Salesforce Certified JavaScript Developer I until Salesforce dropped the "I" from the name. It follows the official exam outline section by section, with plain-English explanations, runnable code snippets, original diagrams, quick-reference tables, a worked project example and 25 practice questions with answers and explanations.

Here's the thing that surprises most Salesforce developers: this is a JavaScript exam first and a Salesforce exam a distant second. You won't be asked about Apex, SOQL or governor limits. You'll be asked what a block of code logs, why a test doesn't prove anything, which array method leaves the original alone, and when a promise callback runs compared to a timer.

Updated for 2026: checked against the current official exam guide, which still aligns the exam to the Summer '22 release, and against Salesforce's Superbadge Retirement FAQ. Since October 1, 2025, you earn the credential by passing the exam alone; the Lightning Web Components Specialist superbadge is no longer required. The outline, weights and passing score below are what you'll actually be tested on.

SectionWeightApprox. questions
Variables, Types, and Collections23%~14
Objects, Functions, and Classes25%~15
Browser and Events17%~10
Debugging and Error Handling7%~4
Asynchronous Programming13%~8
Server Side JavaScript8%~5
Testing7%~4
Exam factDetail
Official nameSalesforce Certified JavaScript Developer (formerly Salesforce Certified JavaScript Developer I)
Exam codeJS-Dev-101, as listed on the Trailhead Academy registration page (the exam guide itself doesn't print a code)
Format60 scored multiple-choice/multiple-select questions, plus up to 5 unscored questions
Time105 minutes
Passing score65% (about 39 of 60 scored questions)
FeeUS$200 (JPY 30,000) to register, US$100 (JPY 15,000) to retake, plus applicable taxes
PrerequisiteNone
SuperbadgeNot required since October 1, 2025, when Salesforce retired the Lightning Web Components Specialist superbadge requirement (Superbadge Retirement FAQ)
Release alignmentSummer '22, per the current exam guide
LanguagesEnglish, Japanese, French and Spanish, per the Trailhead Academy listing
DeliveryOnsite at a testing center or online proctored; no reference materials allowed
MaintenanceOne Trailhead certification maintenance badge per year
Official resourcesExam guide, credential page and the official prep Trailmix

Bar chart of Salesforce JavaScript Developer exam weights: Variables, Types, and Collections 23%, Objects, Functions, and Classes 25%, Browser and Events 17%, Debugging and Error Handling 7%, Asynchronous Programming 13%, Server Side JavaScript 8%, Testing 7%

The first two sections are almost half the exam, and both are pure language fundamentals.

Contents

  1. Who this certification is for
  2. What changed for 2026
  3. How to use this guide
  4. The JavaScript runtime in one picture
  5. Variables, Types, and Collections (23%)
  6. Objects, Functions, and Classes (25%)
  7. Browser and Events (17%)
  8. Debugging and Error Handling (7%)
  9. Asynchronous Programming (13%)
  10. Server Side JavaScript (8%)
  11. Testing (7%)
  12. Deep dive: design decisions and code tracing
  13. Worked example: a JavaScript order tracker, end to end
  14. Hands-on checklist
  15. Common exam traps
  16. Flashcard terms
  17. Mixed practice exam: 4 more questions
  18. Quick-reference cheat sheet
  19. Frequently asked questions
  20. Related study guides

Who this certification is for

Salesforce describes the candidate as someone with 1-2 years of experience developing front-end and/or back-end JavaScript applications for the web stack, who can design, develop and test solutions that are performant, maintainable and reusable. The exam guide is explicit that the skills apply to any framework, such as the Lightning Component framework, and aren't mobile or device specific.

The guide lists the topics candidates typically have experience with. Read it as your syllabus:

  • Data types and operators
  • Variables and variable scope
  • String manipulation
  • Type coercion and conversion, both implicit and explicit
  • Functions, including higher-order functions
  • Data structures: objects and arrays
  • Inheritance, classes and modules
  • The Document Object Model (DOM) and events
  • Error handling and debugging
  • Testing, platform agnostic
  • Asynchronous programming
  • Server-side JavaScript (Node.js)

Salesforce lists roles from JavaScript programmer and Salesforce Developer to full stack, front end and UI/UX engineers. In practice, I see three groups take it:

  • Salesforce developers who build Lightning Web Components and want proof they understand the language under the framework. This guide speaks to this group most.
  • Web developers moving into Salesforce. If you've written React, Vue or plain JavaScript for a few years, this credential rewards what you already know.
  • Admins and Flow builders learning to code. It's a tougher first step than it looks, so give yourself extra weeks and write code every day.

If you're choosing between this and Platform Developer (formerly Platform Developer I): Platform Developer covers Apex, SOQL, data modeling and some LWC. JavaScript Developer goes much deeper on the language itself. Many Salesforce developers hold both, and they complement each other well.

What changed for 2026

The exam outline itself has been stable for a while, but several things around it have moved. Here's what matters if you're studying now:

  • The superbadge is gone. For years, this credential had two parts: the proctored exam and the Lightning Web Components Specialist superbadge. Salesforce retired that superbadge requirement on October 1, 2025. The exam itself didn't change; if a course tells you to finish the superbadge first, it predates the change. I wrote about the broader shift in Trailhead Superbadge Overhaul.
  • The name dropped the "I". The credential page and exam guide now say Salesforce Certified JavaScript Developer. Trail and Trailmix URLs still say "javascript-developer-i", and you'll see both names in study material. I couldn't find an official announcement date for the rename, so I'm not going to guess one.
  • The decorator objective is no longer listed. Older versions of the exam guide had an objective about JavaScript decorators. The current Objects, Functions, and Classes section lists five objectives, and decorators aren't one of them. I mention the LWC decorators only briefly.
  • The exam still aligns to Summer '22. The language has kept moving since then. Newer additions such as toSorted() (ES2023) and Object.groupBy() (ES2024) are worth knowing, but don't expect the exam to test features newer than its alignment date.

How to use this guide

This is a code-reading exam. Most questions show a short snippet or describe a requirement, then ask what the code outputs, which implementation fits, or which option fixes a bug. You need to run code in your head accurately, and the way to get there is to run a lot of real code first.

  1. Open a browser console or install Node.js and type every snippet in this guide. Change one thing, predict the result, then run it. That loop is the whole game.
  2. Read one section at a time. Each ends with a key takeaway and practice questions.
  3. Read every answer explanation, including why the wrong answers are wrong.
  4. Some real exam questions are multiple-select ("choose two"). The practice questions here are single-answer, so when you review, also ask yourself which other options would be correct if the question said "choose two".
  5. In the last week, do the worked example, the mixed practice set and the cheat sheet. On exam day, pace yourself: 60 questions in 105 minutes is 1 minute 45 seconds per question.

If you want the learning-science reasons for this order (retrieval practice, spacing, interleaving), read The Science of Studying for Salesforce Certifications. For daily practice reps, the idea in Code Katas works especially well for this exam.

Six-week Salesforce JavaScript Developer study plan: week 1 variables, types and collections, week 2 objects, functions and classes, week 3 browser and events, week 4 asynchronous programming, week 5 Node.js, debugging and testing, week 6 practice and review

A six-week plan for someone with a year or two of JavaScript.

The JavaScript runtime in one picture

Every section of this exam lives somewhere in one mental model. If you can picture it, output-prediction questions get much easier:

  1. The engine (V8 in Chrome and Node.js) runs your code on a single call stack, one function frame at a time. Variables, scope and this live here.
  2. The heap holds objects, arrays, functions and closures. Variables that point at objects hold references into the heap, which is why copying an object variable doesn't copy the object.
  3. The host environment adds the APIs the language doesn't have on its own. The browser gives you the DOM, events, fetch, timers and storage. Node.js gives you fs, http, process and streams. setTimeout isn't part of the JavaScript language; it's a host API.
  4. Queues hold callbacks waiting to run. The microtask queue holds promise reactions and await continuations. The task queue (often called the macrotask or callback queue) holds timers, UI events and I/O callbacks.
  5. The event loop moves work from the queues to the stack. When the stack is empty, it runs every microtask, lets the browser render if needed, then takes one task, and repeats.
  6. Around all of this sit your tools: DevTools for debugging, try/catch for errors, and a test runner such as Jest.

How JavaScript runs: the engine with its call stack and heap, host environment APIs for the browser (DOM, events, fetch, timers, storage) and Node.js (fs, path, http, events, streams, process), the microtask queue for promise reactions, the task queue for timers, events and I/O, and the event loop that runs all microtasks and then the next task; each exam section is tagged with its weight

Variables and Objects live on the stack and heap, Browser and Server live in the host APIs, and Async lives in the queues.

Variables, Types, and Collections (23%)

This section tests declarations, type conversion, and working with strings, numbers, dates, arrays and JSON. The official objectives:

  • Given a scenario, write code to create variables and initialize them correctly.
  • Given a business requirement, utilize strings, numbers, and dates effectively.
  • Given a scenario or example, demonstrate awareness of type coercion and its effects.
  • Given a specific scenario, distinguish truthy or falsey evaluations.
  • Given a list of data, demonstrate data manipulation with arrays.
  • Given a JSON response, demonstrate how to operate the JSON object.

Declaring variables. Use const by default, let when the binding must change, and treat var as legacy you need to read but shouldn't write. const must be initialized on the same line, and it protects the binding, not the value: you can't reassign a const array, but you can push to it. Assigning to a name you never declared throws a ReferenceError in strict mode (which includes classes and modules) and silently creates a global in old sloppy-mode scripts. The scope and hoisting rules for all three keywords are in the deep dive.

const taxRate = 0.08;          // must initialize; cannot reassign
let attempts = 0;              // will change
attempts += 1;
const cart = [];               // binding is constant...
cart.push('tent');             // ...contents are not
const { city = 'Denver', zip } = { zip: '80202' };   // destructuring with a default
let unset;                     // undefined

The types. JavaScript has seven primitive types and one structural type, object. Primitives are immutable and compared by value. Objects (including arrays, functions, dates, maps and sets) are compared by reference. typeof has two famous quirks the exam loves: typeof null is "object" (a bug kept for compatibility), and typeof on a function is "function" even though functions are objects. Use Array.isArray() to detect arrays, because typeof [] is "object".

TypeExampletypeof resultWatch out for
string'tent', template literals"string"Immutable; methods return new strings
number42, 3.14, NaN, Infinity"number"Floating point: 0.1 + 0.2 !== 0.3
bigint9007199254740993n"bigint"Can't mix with number in arithmetic (TypeError)
booleantrue, false"boolean"Many values coerce to booleans in conditions
undefineddeclared but unassigned"undefined"Also what missing properties and missing arguments return
nullintentional "no value""object"The famous typeof bug
symbolSymbol('id')"symbol"Unique keys; skipped by JSON.stringify and for…in
object{}, [], functions, dates"object" or "function"Copied and compared by reference

Primitives copy values while objects copy references: with let a = 1, let b = a, b = 2, a stays 1; with const y = x and y.n = 2, both x and y point at the same heap object so x.n is also 2; spread makes a shallow copy and structuredClone makes a deep copy

Assigning an object never copies it. Spread copies one level; structuredClone copies all the way down.

Numbers. All number values are 64-bit floating point. That gives you 0.1 + 0.2 equal to 0.30000000000000004, so compare money in cents (integers) or round with care. toFixed(2) returns a string, not a number. Integers are only exact up to Number.MAX_SAFE_INTEGER (2^53 – 1); beyond that, use BigInt. NaN is the only value not equal to itself, so test it with Number.isNaN().

Conversion'42px''' (empty)null'0x1A'
Number(x)NaN0026
parseInt(x, 10)42NaNNaN0
parseFloat(x)42NaNNaN0
Unary +xNaN0026

Number() and unary plus convert the whole string or give NaN; parseInt and parseFloat read from the left and stop at the first character they can't use.

Strings. Strings are immutable: every method returns a new string. Know these well, especially the ones with similar names:

NeedMethodNote
Part of a stringslice(start, end)Accepts negative indexes from the end
Part of a string (older)substring(start, end)Treats negatives as 0 and swaps arguments if start > end
Contains / starts / endsincludes(), startsWith(), endsWith()Return booleans; case sensitive
PositionindexOf(), lastIndexOf()Return -1 when not found
Replace every matchreplaceAll() or a regex with the g flagreplace() with a string replaces only the first match
Split and joinsplit(','), array.join(', ')split('') gives single characters
Pad, trim or last characterpadStart(5, '0'), trim(), at(-1)at() accepts negative indexes

Dates. The Date object has sharp edges. Months are zero-based, so new Date(2026, 0, 31) is January 31. getDate() is the day of the month, while getDay() is the day of the week (0 is Sunday). A date-only ISO string such as new Date('2026-03-15') is parsed as UTC midnight, while '2026-03-15T10:00' (no offset) is parsed as local time. Date objects are mutable (setDate() changes them in place), and dates subtract to milliseconds.

const due = new Date(2026, 0, 31);        // Jan 31, 2026, local time
due.setDate(due.getDate() + 1);           // mutates: now Feb 1
const days = (new Date(2026, 1, 15) - due) / 86400000;   // 14
new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(due);  // 'Feb 1, 2026'

Type coercion. Coercion is JavaScript converting a value from one type to another, either explicitly (String(x), Number(x), Boolean(x)) or implicitly because an operator needs a different type. The rules the exam leans on:

  • + with a string on either side concatenates. '5' + 3 is '53', and 1 + 2 + '3' is '33' because evaluation goes left to right.
  • The other arithmetic operators convert to numbers. '5' - 3 is 2, '5' * '2' is 10, and 'abc' - 1 is NaN.
  • == coerces, === doesn't. '' == 0, '1' == 1 and null == undefined are all true. But null == 0 is false, because null only loosely equals undefined. Use === everywhere unless you deliberately want the x == null shortcut that catches both null and undefined.
  • Objects convert through valueOf() and toString(). That's why [1, 2] + [3] is '1,23' and String({}) gives '[object Object]'.
  • Object.is() is like === except that Object.is(NaN, NaN) is true and Object.is(0, -0) is false. Jest's toBe() uses it.

Truthy and falsy. In an if, a ternary, &&, || or !, every value is treated as true or false. There are exactly eight falsy values: false, 0, -0, 0n, '', null, undefined and NaN. Everything else is truthy, including '0', 'false', [], {} and new Boolean(false). Remember that && and || return one of their operands, not a boolean: '' || 'guest' is 'guest' and 'Ana' && 'Ana!' is 'Ana!'. When 0 or an empty string is a valid value, use the nullish coalescing operator ??, which only falls back on null or undefined.

Truthy, falsy and coercion: the eight falsy values are false, 0, -0, 0n, the empty string, null, undefined and NaN; everything else is truthy including the string 0, the string false, empty arrays and empty objects; examples show string 5 plus 3 gives 53, string 5 minus 3 gives 2, null loosely equals undefined, NaN is not equal to NaN, and unary plus trims and converts a padded numeric string

Plus with any string concatenates. Every other arithmetic operator converts to numbers.

Arrays. Arrays are ordered, zero-indexed, can hold mixed types and have a writable length (setting it shorter truncates the array). Watch out for new Array(3), which creates three empty slots rather than [3]. The most tested idea is whether a method mutates the original array or returns a new value.

Array methods that mutate the original array: push, pop, shift, unshift, splice, sort, reverse, fill and copyWithin; methods that return a new value: map, filter, reduce, slice, concat, flat, flatMap, find, findIndex, some, every, includes, join, toSorted, toReversed and with; default sort converts items to strings; a typical pipeline filters paid orders, maps to totals and reduces to a sum

If a question asks what the original array looks like afterward, check this list first.

RequirementMethodReturns
Transform every itemmap(fn)New array, same length
Keep items that matchfilter(fn)New array, possibly shorter
Combine into one valuereduce(fn, initial)Whatever the accumulator becomes
First matchfind(fn) / findIndex(fn)The item or undefined / the index or -1
Any match / all matchsome(fn) / every(fn)Boolean
Contains a valueincludes(x)Boolean; finds NaN, unlike indexOf
Copy partslice(start, end)New array; original untouched
Insert or remove in placesplice(start, count, ...items)Array of removed items; original mutated
Sort numbers safely[...arr].sort((a, b) => a - b) or toSorted()Sorted copy
Run side effectsforEach(fn)undefined; you can't break out early

Always pass reduce an initial value: without one, it uses the first element as the accumulator, and on an empty array it throws a TypeError. And the default sort() compares items as strings, so [10, 9, 1].sort() gives [1, 10, 9]; pass a compare function such as (a, b) => a - b.

JSON. JSON is a text format, and the JSON object converts between that text and JavaScript values. JSON.parse(text) throws a SyntaxError on invalid input (single quotes, trailing commas and unquoted keys are all invalid JSON), so wrap untrusted input in try/catch. parse takes an optional reviver function, and JSON.stringify(value, replacer, space) takes an optional replacer (a function or an array of allowed keys) and an indent. If an object has a toJSON() method, stringify uses its return value; that's how Date becomes an ISO string.

Value inside an objectWhat JSON.stringify does
undefined, functions, symbolsProperty is omitted (inside an array they become null)
NaN, InfinityBecomes null
DateISO 8601 string, via toJSON()
Map, SetBecomes {}: their entries aren't own enumerable properties
BigIntThrows a TypeError
Circular referenceThrows a TypeError

That table explains why JSON.parse(JSON.stringify(obj)) is a lossy deep copy: dates come back as strings, and undefined values and methods disappear. Use structuredClone() for a real deep copy of data (it handles dates, maps, sets and circular references, but throws on functions).

const raw = '{"id":"A-17","placed":"2026-04-02T15:30:00.000Z","lines":[{"sku":"TENT-2","qty":1}]}';
const order = JSON.parse(raw, (key, value) =>
  key === 'placed' ? new Date(value) : value);
order.placed.getUTCHours();                    // 15
JSON.stringify(order, ['id', 'lines', 'sku'], 2);   // only listed keys, indented

Key takeaway: this section rewards precision. Know the eight falsy values, which operators coerce and how, which array methods mutate, and exactly what JSON.stringify drops.

Practice questions: Variables, Types, and Collections

Question 1. A checkout widget reads quantity from a text input and price from an API. A developer logs three values while debugging. What appears in the console?

const qty = '3';      // from an <input>, always a string
const price = 4;
console.log(qty + price, qty * price, `${qty - price}`);
  • A. 34 12 -1
  • B. 7 12 -1
  • C. 34 34 -1
  • D. 7 12 1

Answer: A. qty + price has a string operand, so + concatenates to '34'. * converts both sides to numbers, giving 12. Inside the template literal, - also converts to numbers, so 3 - 4 is -1, which the template turns into the string '-1'. Why not the others: B assumes + converts the string to a number, which only happens when neither operand is a string; C assumes * concatenates, but only + does; D gets the subtraction backward.

Question 2. A leaderboard needs the scores in rank order for display, but the original scores array must stay in submission order for an audit log. A developer writes the code below. What are scores and ranked afterward?

const scores = [40, 100, 9, 25];
const ranked = scores.sort();
  • A. scores is unchanged; ranked is [9, 25, 40, 100]
  • B. Both are [9, 25, 40, 100]
  • C. Both are [100, 25, 40, 9], and they're the same array
  • D. scores is unchanged; ranked is [100, 25, 40, 9]

Answer: C. sort() sorts in place and returns the same array, so ranked === scores. With no compare function it compares items as strings, and '100' < '25' < '40' < '9'. The fix is [...scores].sort((a, b) => a - b) or scores.toSorted((a, b) => a - b). Why not the others: A and D wrongly assume sort() returns a copy; B gets the mutation right but assumes numeric sorting, which needs a compare function.

Question 3. An integration sends an order to a REST API with JSON.stringify. What string is sent?

const order = {
  id: 7,
  note: undefined,
  placed: new Date(Date.UTC(2026, 0, 15)),
  total: NaN,
  format() { return `#${this.id}`; }
};
console.log(JSON.stringify(order));
  • A. {"id":7,"placed":"2026-01-15T00:00:00.000Z","total":null}
  • B. {"id":7,"note":undefined,"placed":{},"total":NaN}
  • C. {"id":7,"note":null,"placed":"2026-01-15T00:00:00.000Z","total":null,"format":null}
  • D. It throws a TypeError because the object contains a function

Answer: A. Properties whose values are undefined or functions are omitted. The Date serializes through its toJSON() method to an ISO string, and NaN becomes null. Why not the others: B shows values JSON can't represent (undefined and NaN aren't valid JSON); C converts omitted properties to null, which only happens inside arrays; D confuses functions (silently skipped) with BigInt or circular references (which do throw).

Question 4. A pricing function receives an options object. A discount of 0 is valid and must be kept, but when no discount is provided at all, the function should use 10. Which operator should replace the blank?

function getDiscount(options) {
  return options.discount ____ 10;
}
  • A. ||
  • B. ??
  • C. &&
  • D. |

Answer: B. The nullish coalescing operator ?? falls back only when the left side is null or undefined, so getDiscount({ discount: 0 }) returns 0 and getDiscount({}) returns 10. Why not the others: || treats 0 as falsy and would return 10 for a real zero discount (A); && returns undefined when the property is missing (C); bitwise OR converts to integers, so 0 | 10 is 10 (D).

Objects, Functions, and Classes (25%)

The biggest section on the exam. The official objectives:

  • Given a business requirement, locate the best function implementation.
  • Given a business requirement, apply fundamentals of object implementation to solve the business requirement.
  • Given a business requirement, apply fundamentals of class implementation to solve the business requirement.
  • Given a JavaScript module, give examples of how to use the module.
  • Given a block of code, analyze the variable scope and the execution flow.

Function forms. JavaScript gives you several ways to write a function, and they differ in hoisting, this and whether they can be used as constructors. Most "which implementation" questions come down to this table:

FormExampleHoisted?Own this?Use with new?
Declarationfunction total(a, b) {}Yes, fully: callable before the lineYes, set by the callYes
Expressionconst total = function () {};Only the variable (TDZ for let/const)YesYes
Arrowconst total = (a, b) => a + b;Only the variableNo: inherits from the enclosing scopeNo (TypeError)
Method shorthand{ total() {} } or in a classWith the object or classYes, usually the objectNo
Classclass Cart {}Hoisted but in the TDZYes (instances)Required

Arrow functions also have no arguments object, which makes them ideal for callbacks and a poor choice for methods that need this.

Parameters. Default parameters (function ship(order, carrier = 'UPS')) apply only when the argument is undefined, not when it's null. Rest parameters (...items) collect the remaining arguments into a real array. Spread (fn(...list)) does the opposite at the call site. Missing arguments are simply undefined.

Higher-order functions and closures. A higher-order function takes a function as an argument or returns one. map, filter, addEventListener and setTimeout all take functions. A closure is a function plus the variables it captured from the scope where it was created; those variables stay alive as long as the function does.

function debounce(fn, wait) {
  let timer;                          // private, captured by the returned function
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}
const onSearch = debounce((term) => console.log('search', term), 300);

That's a good "best function implementation" example: a closure keeps timer private, and the inner arrow function forwards the caller's this and arguments.

Objects. Object literals support shorthand properties ({ name }), shorthand methods ({ total() {} }), computed keys ({ [field]: value }), getters and setters, and spread. Property access uses dots or brackets; brackets take any expression. Optional chaining (order?.customer?.email) returns undefined instead of throwing when something in the chain is null or undefined.

NeedUseNote
Keys, values or pairsObject.keys(), Object.values(), Object.entries()Own enumerable string keys only
Pairs back to an objectObject.fromEntries()Great after entries() plus map or filter
Shallow copy or merge{ ...a, ...b } or Object.assign({}, a, b)Later sources win; nested objects are shared
Deep copy of datastructuredClone(obj)Throws on functions
Does it have its own key?Object.hasOwn(obj, 'k')'k' in obj also checks the prototype chain
Prevent all changesObject.freeze(obj)Shallow; in strict mode, writes throw a TypeError
Fine-grained property controlObject.defineProperty()Unspecified flags default to false: not writable, enumerable or configurable

How this works. this is decided by how a function is called, not where it's written, with one exception: arrow functions don't have their own this and use the one from the surrounding scope. The classic bug is passing a method as a callback, such as setTimeout(cart.checkout, 0) or button.addEventListener('click', this.handleClick). The method is called on its own, so this is lost. Fix it with an arrow wrapper (() => cart.checkout()), bind, or a class field arrow function.

How this is decided, checked in order: an arrow function uses this from the surrounding scope and call, apply and bind cannot change it; a function called with new gets the newly constructed object; a function called with call, apply or bind gets the object passed as the first argument; a function called as obj.method() gets the object before the dot; a plain call gets undefined in strict mode, classes and modules, or globalThis in sloppy scripts

Find the call site, then walk these rules from the top.

MethodCalls the function now?ArgumentsTypical use
fn.call(obj, a, b)YesListed one by oneBorrow a method for one call
fn.apply(obj, [a, b])YesAs an arrayForward an arguments-style list
fn.bind(obj, a)No: returns a new functionOptional, pre-filledCallbacks and partial application; can't be re-bound

Prototypes and inheritance. Every object has an internal link to a prototype object. When you read a property that isn't on the object itself, JavaScript walks up that prototype chain until it finds the property or reaches null. Class methods live on ClassName.prototype, shared by every instance. Object.create(proto) makes an object with a chosen prototype, and Object.getPrototypeOf(obj) reads it. Writing a property creates an own property and never changes the prototype.

The prototype chain for an instance rex of class Dog extends Animal: rex holds the own property name, Dog.prototype holds bark, Animal.prototype holds eat, Object.prototype holds toString and hasOwnProperty, and the chain ends at null; calling rex.eat() is not found on rex or Dog.prototype and is found on Animal.prototype

Classes are a cleaner syntax for this same prototype chain.

Classes. A class is syntax over constructor functions and prototypes, with a few stricter rules: class bodies always run in strict mode, classes can't be called without new, and class declarations are hoisted into the temporal dead zone, so using one before its line throws a ReferenceError. The parts to know:

  • constructor runs on new. In a subclass, you must call super(...) before touching this, or you get a ReferenceError.
  • Methods go on the prototype and aren't enumerable. Static methods and properties live on the class itself (Cart.fromJSON()), not on instances.
  • Public fields (count = 0;) are created on each instance. Private fields and methods start with # (#balance) and can only be read inside the class body; trying from outside is a SyntaxError.
  • Getters and setters (get total() {}, set total(v) {}) look like properties to callers, which is handy for computed values and validation.
  • extends and super set up inheritance; super.method() calls the parent's version from an override.
  • instanceof checks whether a constructor's prototype is in an object's chain.
class Account {
  #balance = 0;                         // private field
  static count = 0;                     // shared by the class
  constructor(owner) { this.owner = owner; Account.count++; }
  get balance() { return this.#balance; }
  deposit(amount) {
    if (amount <= 0) throw new RangeError('Deposit must be positive');
    this.#balance += amount;
    return this;                        // enables chaining
  }
}
class Savings extends Account {
  constructor(owner, rate) { super(owner); this.rate = rate; }   // super first
}
const s = new Savings('Ana', 0.02).deposit(100);
s.balance;                // 100
s instanceof Account;     // true

Modules. ES modules are files that export values and import them from other files. They run in strict mode, have their own top-level scope (no accidental globals), are evaluated once and cached no matter how many files import them, and expose live, read-only bindings: if the exporting module changes an exported let, importers see the new value, but importers can't reassign it. In Node.js, a file is treated as an ES module when it ends in .mjs or the nearest package.json has "type": "module"; otherwise it's CommonJS, which uses require() and module.exports.

ES module patterns: named exports imported by exact name or renamed with as, one default export imported with any local name, namespace imports with import star as, dynamic import that returns a promise, re-exports for a single public entry point, and rules that modules are strict, evaluated once and cached, with live read-only imports and an undefined top-level this

Named imports must match the export name. A default import can use any name.

Lightning Web Components are ES modules too. import { LightningElement, api, wire } from 'lwc'; is a named import, and each component file default-exports its class. Decorators such as @api aren't standard JavaScript yet; LWC compiles them.

Scope and execution flow. JavaScript has global, module, function and block scope, and names resolve from the inside out. Because these are the questions people most often get wrong, they get their own walkthrough in the deep dive.

Key takeaway: for any function, know who calls it and how, because that decides this. For any object, know whether you're holding the object or a copy. For any module, know whether you need a named or default import.

Practice questions: Objects, Functions, and Classes

Question 1. A dashboard shows how long a user has been on a page. The counter never increases, and this.seconds is still 0 after a minute. What's the best fix?

class Timer {
  constructor() { this.seconds = 0; }
  start() {
    setInterval(function () {
      this.seconds++;
    }, 1000);
  }
}
new Timer().start();
  • A. Add 'use strict' at the top of the class
  • B. Declare seconds with let inside start()
  • C. Move the increment into a regular method tick() and call setInterval(this.tick, 1000)
  • D. Replace the regular function passed to setInterval with an arrow function: () => { this.seconds++; }

Answer: D. The regular function passed to setInterval gets its own this from the timer call: the global object in browsers, a Timeout object in Node.js. It's never the Timer instance. An arrow function has no this of its own, so it uses the this of start(), which is the instance. Why not the others: a local let wouldn't be the property the dashboard reads (B); passing this.tick unbound loses this in exactly the same way (C); class bodies are already strict, and strict mode doesn't change how the timer calls the function (A).

Question 2. A developer builds a small counter factory for two independent widgets. What does the last line log?

function makeCounter() {
  let count = 0;
  return { inc: () => ++count, get: () => count };
}
const a = makeCounter();
const b = makeCounter();
a.inc(); a.inc(); b.inc();
console.log(a.get(), b.get());
  • A. 2 1
  • B. 3 3
  • C. 2 2
  • D. 1 1

Answer: A. Each call to makeCounter() creates a new count variable, and the returned arrow functions close over that specific variable. Counter a was incremented twice and b once, so the result is 2 1. Why not the others: B assumes a single shared count, which would only happen if it were declared outside the factory; C and D ignore that each counter keeps its own state.

Question 3. A developer creates a Bundle class that extends Product. What happens when the last line runs?

class Product {
  constructor(name) { this.name = name; }
  label() { return `Product: ${this.name}`; }
}
class Bundle extends Product {
  constructor(name, items) {
    this.items = items;
    super(name);
  }
}
const b = new Bundle('Starter', 3);
  • A. A ReferenceError is thrown, because this is used before super() is called
  • B. b.items is 3 and b.name is 'Starter'
  • C. The object is created, but b.name is undefined
  • D. A SyntaxError is thrown when the file is parsed

Answer: A. In a derived class constructor, this doesn't exist until super() runs. Touching it first throws a ReferenceError at runtime when the constructor executes. Moving super(name) to the first line fixes it. Why not the others: B is the intended result only after the fix; C describes forgetting to pass name to super, not this bug; D is wrong because the code is syntactically valid, and the error only appears when new Bundle() runs.

Question 4. money.js contains export default function formatMoney(cents) {} and export const CURRENCY = 'USD';. Another ES module needs both. Which statement is correct?

  • A. import formatMoney, { CURRENCY } from './money.js';
  • B. import { formatMoney, CURRENCY } from './money.js';
  • C. import * as formatMoney from './money.js'; then call formatMoney(500)
  • D. const { formatMoney, CURRENCY } = require('./money.js');

Answer: A. A default export is imported without braces, under any name you like, and named exports go inside braces. You can combine both in one statement. Why not the others: there's no named export called formatMoney, so B fails when the module links; a namespace import gives an object, so the function is formatMoney.default, and calling the namespace throws a TypeError (C); require is CommonJS and isn't available inside an ES module by default (D).

Question 5. A strict-mode module freezes its configuration so other code can't change it. Then two lines try to change it. What happens?

'use strict';
const config = Object.freeze({
  retries: 3,
  endpoints: { orders: '/v1/orders' }
});
config.endpoints.orders = '/v2/orders';
config.retries = 5;
  • A. Both assignments succeed, because const objects can be modified
  • B. Both assignments throw a TypeError
  • C. The first assignment succeeds; the second throws a TypeError
  • D. The first assignment throws; the second is silently ignored

Answer: C. Object.freeze() is shallow. The nested endpoints object isn't frozen, so changing orders works. retries is a property of the frozen object, and in strict mode writing to it throws a TypeError (in sloppy mode it would fail silently). To freeze everything, freeze nested objects recursively. Why not the others: A ignores the freeze, and const is irrelevant to object mutability anyway; B assumes freeze is deep; D reverses which object is frozen.

Browser and Events (17%)

The official objectives:

  • Given a business requirement, utilize Events, event handlers, and propagation.
  • Given a business requirement, evaluate and manipulate the DOM.
  • Given a scenario, utilize the Browser Dev Tools to investigate code behavior.
  • Given a scenario and requirements, utilize browser specific APIs.

The DOM. The Document Object Model is the browser's live tree of objects representing the page. document is the root you start from. Selecting elements is the first skill:

MethodReturnsLive or static?
document.getElementById('cart')One element or nulln/a
document.querySelector('.item.active')First match for any CSS selector, or nulln/a
document.querySelectorAll('li')A NodeList (has forEach)Static: doesn't update when the page changes
document.getElementsByClassName('item')An HTMLCollectionLive: updates automatically
el.closest('li')Nearest ancestor (or itself) matching the selectorn/a
el.matches('.danger')Booleann/a

Reading and changing content. textContent gets or sets all text, including hidden text, and is the safe, fast default. innerText is layout-aware and skips hidden text. innerHTML parses a string as HTML, which is convenient and dangerous: inserting user input with it opens the door to cross-site scripting (XSS). Classes are easiest through classList.add(), remove(), toggle() and contains(). Custom data-* attributes appear on dataset in camelCase, so data-order-id becomes el.dataset.orderId.

Creating and removing elements. Use document.createElement('li'), set its properties, then insert it with append(), prepend(), before(), after() or the older appendChild(). Remove with el.remove(). To add many elements, build them in a DocumentFragment and insert once.

const list = document.querySelector('#orders');
const frag = document.createDocumentFragment();
for (const order of orders) {
  const li = document.createElement('li');
  li.textContent = `${order.id}: ${order.status}`;   // safe: no HTML parsing
  li.dataset.orderId = order.id;                     // data-order-id="..."
  li.classList.toggle('late', order.daysLate > 0);
  frag.append(li);
}
list.replaceChildren(frag);    // one insertion, one layout

Events and handlers. Prefer addEventListener(type, listener, options) over onclick properties: you can attach several listeners to one event, and you control the options. The useful options are capture, once, passive and signal. To remove a listener with removeEventListener, you must pass the same function reference and the same capture setting, so an inline arrow function can't be removed later.

Inside a handler, the event object tells you what happened:

Property or methodWhat it does
event.targetThe element where the event started (the innermost element clicked)
event.currentTargetThe element whose listener is running right now
event.preventDefault()Cancels the browser's default action, such as submitting a form or following a link; propagation continues
event.stopPropagation()Stops the event from moving to further elements; other listeners on the current element still run
event.stopImmediatePropagation()Also stops the remaining listeners on the current element
event.type, event.key, event.detailThe event name, the key pressed, or the custom payload

Propagation. A DOM event travels in three phases. In the capture phase it goes from window down through each ancestor toward the target. In the target phase it reaches the element itself. In the bubble phase it travels back up to window. Listeners run during bubbling by default. Most events bubble, but some don't: focus, blur, mouseenter, mouseleave and resource load events don't. Use focusin and focusout when you need bubbling focus events.

Event propagation for a click on a button inside a list inside the body: in the capture phase the event travels from window through document, body and the list down to the button; in the target phase listeners on the button run; in the bubble phase the event travels back up to window; addEventListener with capture true listens on the way down, stopPropagation stops the journey, preventDefault only cancels the default action, and delegation puts one listener on the list and uses event.target.closest

Listeners run on the way up by default. Capture listeners run on the way down.

Event delegation uses bubbling to handle events for many children with one listener on a shared parent. It also works for children added later. The catch is that event.target may be a nested element, such as an icon inside a button, so use event.target.closest('button') to find the element you care about.

Custom events. Create your own with new CustomEvent('orderselected', { detail: { id }, bubbles: true }) and send it with element.dispatchEvent(event). In Lightning Web Components, this is how a child talks to its parent: the child calls this.dispatchEvent(new CustomEvent('select', { detail: this.recordId })), and the parent listens in its template with onselect={handleSelect}. Because LWC uses shadow DOM, an event only crosses the shadow boundary with composed: true, and outside listeners see the component host as event.target (retargeting).

Browser DevTools. The exam expects you to know which tool investigates which problem:

ProblemDevTools panel or feature
Inspect or live-edit HTML and CSS; see listeners attached to an elementElements (with the Event Listeners tab)
Run code, read logs and errors, inspect objectsConsole
Pause code, step through it, watch variablesSources (breakpoints, call stack, scope, watch)
See requests, status codes, payloads and timingNetwork
Find slow scripts, long tasks and layout workPerformance
Inspect localStorage, sessionStorage, cookies, IndexedDBApplication

Browser-specific APIs. These belong to the browser, not to JavaScript itself, so they don't exist in Node.js (although Node has added its own fetch, timers and URL).

RequirementBrowser APIKey facts
Remember a setting across visitslocalStoragePer origin, strings only (use JSON), about 5 MB, synchronous, never expires
Keep data for one tab sessionsessionStoragePer origin and per tab; cleared when the tab closes
Call an HTTP APIfetch()Returns a promise; only rejects on network failure, so check response.ok
Cancel a request or listenerAbortControllerPass controller.signal; abort() rejects with an AbortError
Change the URL without reloadinghistory.pushState()Listen for popstate on back and forward
Read query parametersnew URL(location.href).searchParamsURLSearchParams handles encoding
Heavy computation off the main threadWeb WorkersNo DOM access; talk through postMessage
async function loadOrders(signal) {
  const response = await fetch('/api/orders?status=open', { signal });
  if (!response.ok) {                         // 404 and 500 still resolve
    throw new Error(`Order API returned ${response.status}`);
  }
  return response.json();                     // also returns a promise
}
const controller = new AbortController();
loadOrders(controller.signal).then(render).catch(showError);
// later, if the user navigates away:
controller.abort();

Key takeaway: know the difference between target and currentTarget, the three propagation phases, which DevTools panel answers which question, and that fetch only rejects when the network fails.

Practice questions: Browser and Events

Question 1. An order list uses one click listener on the <ul> to handle every Cancel button. Each button has a data-order-id attribute and contains an <svg> icon. Cancels work when users click the button's edge but fail with undefined when they click the icon. What's the best fix?

list.addEventListener('click', (event) => {
  cancelOrder(event.target.dataset.orderId);
});
  • A. Call event.stopPropagation() in a listener on the <svg>
  • B. Read event.currentTarget.dataset.orderId instead
  • C. Register the listener with { capture: true }
  • D. Use const btn = event.target.closest('button[data-order-id]') and read btn.dataset.orderId

Answer: D. When the icon is clicked, event.target is the <svg> (or a path inside it), which has no data-order-id. closest() walks up from the clicked element to the nearest matching button, which is exactly what delegation needs. Add a null check for clicks that don't land on a button. Why not the others: currentTarget is the <ul> that owns the listener, which has no order ID (B); capture changes when the listener runs, not what target is (C); stopping propagation at the icon would stop the event from reaching the <ul> listener at all (A).

Question 2. A user clicks the button inside #outer. In what order do the messages appear?

const outer = document.querySelector('#outer');
const button = outer.querySelector('button');
outer.addEventListener('click', () => console.log('outer bubble'));
outer.addEventListener('click', () => console.log('outer capture'), { capture: true });
button.addEventListener('click', () => console.log('button'));
  • A. outer capture, button, outer bubble
  • B. button, outer bubble, outer capture
  • C. outer bubble, outer capture, button
  • D. button, outer capture, outer bubble

Answer: A. The capture phase runs first, from the top down, so the capture listener on #outer fires before the event reaches the button. Then the target's own listener runs, then the event bubbles back up to #outer's regular listener. Registration order doesn't change phase order. Why not the others: B and D run the capture listener after the target, which ignores the capture phase; C assumes listeners run in the order they were added, regardless of phase.

Question 3. A product page loads reviews with the code below. When the reviews API returns 404, users see an empty list instead of the error banner. What change makes the banner appear?

fetch('/api/reviews?product=TENT-2')
  .then((res) => res.json())
  .then((reviews) => renderReviews(reviews ?? []))
  .catch(() => showBanner('Reviews are unavailable'));
  • A. Pass { mode: 'no-cors' } to fetch
  • B. Wrap the fetch call in setTimeout so errors surface later
  • C. In the first then, check res.ok and throw an Error when it's false, before calling res.json()
  • D. Replace .catch() with a second argument to addEventListener

Answer: C. fetch resolves for any HTTP response, including 404 and 500. It only rejects on network failures or an abort. Checking res.ok (true for status 200-299) and throwing turns an HTTP error into a rejection that the existing .catch() handles. Why not the others: setTimeout changes timing, not whether the promise rejects (B); no-cors gives an opaque response you can't read, which makes things worse (A); D mixes up event listeners and promise handling.

Debugging and Error Handling (7%)

A small section by weight, but the skills show up across the whole exam. The official objectives:

  • Given a scenario, handle errors properly.
  • Given code to be debugged, use the console and breakpoints.

try, catch and finally. Code in try runs until something throws. If it throws, control jumps to catch, which receives the thrown value. finally runs no matter what: after a normal finish, after a caught error, and even after a return inside try or catch. If finally itself returns a value, that value replaces whatever try or catch returned, so don't return from finally.

function parseSettings(text) {
  try {
    return JSON.parse(text);
  } catch (err) {
    if (err instanceof SyntaxError) {
      console.warn('Bad settings, using defaults:', err.message);
      return { theme: 'light' };
    }
    throw err;                       // not ours to handle: rethrow
  } finally {
    console.log('parse attempted');  // runs on every path
  }
}

Throw errors, not strings. You can technically throw any value, but Error objects carry a name, a message and a stack trace, which is what makes them debuggable. For domain errors, extend Error and set this.name, then check with instanceof in the catch. Catch only what you can handle, and rethrow the rest.

Error typeTypical cause
TypeErrorCalling something that isn't a function, reading a property of undefined or null, assigning to a frozen property in strict mode, mixing BigInt and number
ReferenceErrorUsing an undeclared variable, or a let, const or class in its temporal dead zone
SyntaxErrorInvalid code at parse time, or invalid text passed to JSON.parse
RangeErrorA value outside the allowed range, such as new Array(-1), (1.5).toFixed(101) or unbounded recursion
AggregateErrorSeveral errors at once, such as when every promise passed to Promise.any rejects
Custom (class ValidationError extends Error)Business rules your own code enforces

Handling errors in JavaScript: try runs code that might throw, catch runs only if something threw, and finally always runs even after return; built-in error types are TypeError, ReferenceError, SyntaxError, RangeError, AggregateError, URIError and EvalError; for async errors, await inside try and catch catches rejections, catch handles a promise chain, unhandled rejections are logged by browsers and end the process in Node by default, and new Error with a cause keeps the original error; a custom ValidationError class extends Error and sets its name

Throw Error objects, catch only what you can handle, and rethrow the rest.

Errors in asynchronous code. A try/catch only catches errors thrown while its block is on the call stack. An error thrown later inside a setTimeout callback or an event handler isn't caught by a try that wrapped the scheduling call. For promises, either await inside a try block or attach .catch(). An unhandled promise rejection triggers the unhandledrejection event in browsers, and in Node.js (version 15 and later) it crashes the process by default.

The console. console.log is the everyday tool, but the exam expects you to know the rest of the toolbox:

MethodUse it to
console.log(), info(), warn(), error()Log at different levels; DevTools can filter by level
console.table(arrayOfObjects)Show records as a sortable table
console.dir(obj)Show an object's properties as an expandable tree, useful for DOM elements
console.time('label') / timeEnd('label')Measure elapsed time between two points
console.assert(condition, msg)Log an error only when the condition is false
console.trace()Print the current call stack

One trap: browsers log objects by reference. If you log an object and then change it, expanding it later in the console may show the new values. Log structuredClone(obj) or JSON.stringify(obj) when you need a snapshot.

Breakpoints. Breakpoints pause execution so you can inspect the call stack, every variable in scope, and the values of watch expressions. Once paused, step over runs the next line, step into enters a function call, step out finishes the current function, and resume continues to the next breakpoint.

Breakpoint typePauses when
Line-of-codeExecution reaches that line
ConditionalThat line runs and an expression is true, such as order.total < 0
LogpointNever pauses; logs a message instead, with no code change
debugger; statementExecution reaches it, but only while DevTools is open
ExceptionsAny exception is thrown, or only uncaught ones

For Lightning Web Components, Salesforce serves minified framework code by default. Turn on Debug Mode for your user in Setup to get readable code in the Sources panel. For Node.js, run node --inspect app.js and attach Chrome DevTools or VS Code.

Key takeaway: catch specific errors and rethrow the rest, remember that finally always runs, and choose a conditional breakpoint or logpoint instead of littering code with console.log.

Practice questions: Debugging and Error Handling

Question 1. A sync job wraps its scheduling code in try/catch so failures appear in the log. What happens when this runs?

try {
  setTimeout(() => {
    throw new Error('Sync failed');
  }, 0);
} catch (e) {
  console.log('Caught:', e.message);
}
console.log('Done');
  • A. Caught: Sync failed is logged, then Done
  • B. Done is logged, then an uncaught Error: Sync failed is reported; the catch block never runs
  • C. Done is logged, then Caught: Sync failed
  • D. Nothing is logged, because the error stops the script before Done

Answer: B. The try block only schedules the callback; it finishes immediately, so Done is logged. When the timer fires later, the callback throws on a new call stack, where no try is active, so the error is uncaught. Put the try/catch inside the callback, or use promises with .catch(). Why not the others: A and C assume the outer try is still active when the callback runs, which it isn't; D assumes the error is thrown synchronously during setTimeout, but scheduling succeeds.

Asynchronous Programming (13%)

The official objectives:

  • Given a scenario, apply asynchronous programming concepts.
  • Given a scenario, use event loop and event monitor or determine loop outcomes.

Why async exists. JavaScript runs on one thread, so waiting synchronously for the network would freeze the page. Instead, you start the work and hand the host a function to call later.

Callbacks. The original style: pass a function that runs when the work is done. Node.js core APIs use error-first callbacks, where the first argument is an error or null: fs.readFile(path, (err, data) => {}). Nesting them produces "callback hell", which is why promises took over.

Promises. A promise is an object representing a value that isn't available yet. It starts pending and settles exactly once, as either fulfilled with a value or rejected with a reason. After that, it never changes. The rules that drive most questions:

  • The executor runs synchronously. In new Promise((resolve, reject) => { ... }), the function you pass runs immediately, during the constructor call. Only the .then callbacks are asynchronous.
  • .then() always returns a new promise. Returning a value from a then callback fulfills the next promise with that value. Returning a promise makes the chain wait for it. Throwing rejects the next promise.
  • .catch(fn) is .then(undefined, fn). A catch handles rejections from anything earlier in the chain, and the chain continues as fulfilled after it unless the catch throws again.
  • .finally(fn) runs on either outcome, receives no argument, and passes the original result or error through.
  • Callbacks are always async. Even for an already-resolved promise, then callbacks run as microtasks after the current synchronous code finishes.

Promise states and combinators: a pending promise settles once as fulfilled, handled by then, or rejected, handled by catch, and finally runs either way; Promise.all resolves when all fulfill with an array of values in input order and rejects with the first rejection; Promise.allSettled waits for all to settle and returns status objects and never rejects; Promise.race settles with the first promise to settle; Promise.any resolves with the first fulfillment and rejects with an AggregateError only if all reject

Choose the combinator by what should happen when one of the promises fails.

RequirementCombinator
Need every result; any failure should fail the whole operationPromise.all
Show whatever succeeded and report what failedPromise.allSettled
Use the fastest of several mirrors; any success will doPromise.any
Enforce a timeout by racing the request against a timerPromise.race

async and await. An async function always returns a promise: returning a value fulfills it, and throwing rejects it. Inside it, await pauses that function (not the whole program) until the awaited promise settles, then resumes with the value or throws the rejection, so ordinary try/catch works. Everything after an await runs as a microtask. Top-level await works in ES modules, but not in classic scripts or CommonJS files.

// Sequential: about 3 seconds if each call takes 1 second
const a = await getInventory();
const b = await getPricing();
const c = await getReviews();

// Parallel: about 1 second; start all three, then wait
const [inv, price, reviews] = await Promise.all([getInventory(), getPricing(), getReviews()]);

// Loops: for...of waits for each; forEach does NOT wait for async callbacks
for (const id of ids) { await saveOne(id); }          // one at a time, in order
await Promise.all(ids.map((id) => saveOne(id)));      // all at once
ids.forEach(async (id) => { await saveOne(id); });    // fire and forget: errors go unhandled

That last line is a favorite exam trap. forEach ignores the promises its async callback returns, so the code after it runs before any save finishes, and rejections become unhandled.

The event loop. This is the second objective, and it's tested with output-order questions. The model:

  1. Run the current script (or task) to completion. Nothing interrupts synchronous code.
  2. When the call stack is empty, run every microtask in the microtask queue, including any new microtasks queued along the way.
  3. In browsers, render if needed (style, layout, paint). requestAnimationFrame callbacks run just before this step.
  4. Take one task from the task queue, such as a timer callback, a UI event or an I/O callback, and run it. Then go back to step 2.
Microtasks (run first)Tasks / macrotasks (run later)
Promise then, catch and finally callbackssetTimeout and setInterval callbacks
Code after await in an async functionUser events such as click and keydown
queueMicrotask(fn)Network and I/O callbacks, postMessage
MutationObserver callbackssetImmediate (Node.js)
process.nextTick (Node.js; runs even before promise microtasks)Script and module evaluation

The event loop decides output order: run all synchronous code, drain every queued microtask, render in the browser if needed, then run one task such as a setTimeout, event or I/O callback and repeat; example code logging A, scheduling B with setTimeout, C with a resolved promise, D with queueMicrotask, then logging E produces A, E, C, D, B

Synchronous first, then all microtasks in the order they were queued, then one task.

Timers deserve a precise definition: setTimeout(fn, 0) means "queue fn as a task after at least 0 ms", not "run now". A busy main thread delays every timer.

The event monitor. The objective's mention of an event monitor maps to tools that show you events as they happen. In the Chrome DevTools Console, monitorEvents(element, 'click') logs every matching event on that element until you call unmonitorEvents(element). The Performance panel records tasks and long tasks, which shows what's blocking the loop.

Key takeaway: an async function returns a promise, await hands the rest of the function to the microtask queue, and microtasks always run before the next timer or event.

Practice questions: Asynchronous Programming

Question 1. What's the console output when this script runs?

async function load() {
  console.log('1');
  await null;
  console.log('2');
}
console.log('3');
setTimeout(() => console.log('4'), 0);
load();
Promise.resolve().then(() => console.log('5'));
console.log('6');
  • A. 3 1 6 2 5 4
  • B. 3 1 2 6 5 4
  • C. 1 3 6 5 2 4
  • D. 3 6 1 2 5 4

Answer: A. Synchronous code first: 3, then calling load() logs 1 synchronously before the await. The await queues the rest of load as a microtask, and the then callback queues another microtask after it. 6 logs synchronously. Then the microtasks run in order: 2, then 5. The timer is a task, so 4 is last. Why not the others: B runs the code after await synchronously, but await always yields, even for a non-promise value; C ignores that console.log('3') runs before load() is called; D assumes an async function doesn't start running until later, but its body runs synchronously up to the first await.

Question 2. A home page loads three independent widgets (weather, news and stock prices) from separate APIs at the same time. If one API fails, the page must still show the other two and display a small error in the failed widget's place. Which approach fits best?

  • A. Promise.all with the three requests
  • B. Promise.allSettled with the three requests, then render each result by its status
  • C. Promise.race with the three requests
  • D. Promise.any with the three requests

Answer: B. allSettled waits for every request to finish and never rejects. Each result is { status: 'fulfilled', value } or { status: 'rejected', reason }, so the page can render successes and show an error for each failure. Why not the others: all rejects as soon as one request fails, discarding the successful results (A); race only gives you the first request to settle (C); any gives you only the first success (D).

Server Side JavaScript (8%)

The official objectives:

  • Given a scenario and requirements, infer which Node.js implementation is a good solution.
  • Given a scenario and requirements, infer which Node.js CLI command is a good solution.
  • Know the core Node.js modules and given requirements, infer which Node.js library/framework is a good solution.
  • Given a scenario and requirements, distinguish which Node.Js Package Management solution is the most fitting.

What Node.js is. Node.js runs JavaScript outside the browser using the V8 engine, plus a C library called libuv that provides the event loop and non-blocking I/O. Your JavaScript still runs on one main thread, but file, network and timer work happens in the background and comes back as callbacks or promises. That makes Node excellent for I/O-heavy work such as APIs, real-time apps, command-line tools and build tooling. It's a weaker fit for long CPU-heavy calculations, which block the event loop unless you move them to worker threads.

RequirementGood Node.js implementation
Process a file too big for memory, line by lineStreams: fs.createReadStream() with readline or stream.pipeline()
Read a small config file without blockingfs/promises (await readFile(path, 'utf8'))
React to things that happen inside your appEventEmitter from the events module
Run a shell command or another programchild_process (exec, execFile, spawn)
Run CPU-heavy work without freezing requestsworker_threads
Keep secrets and settings out of codeEnvironment variables via process.env (and --env-file in Node 20.6+)
Turn an old callback API into promisesutil.promisify()

The CLI. Expect questions that describe a need and ask for the command:

NeedCommand
Run a scriptnode app.js
Check the installed versionnode -v (or node --version)
Debug with Chrome DevTools or VS Codenode --inspect app.js (--inspect-brk pauses on the first line)
Restart automatically when files changenode --watch app.js (built in since Node 18.11; nodemon is the classic package)
Run the built-in test runnernode --test
Load environment variables from a filenode --env-file=.env app.js (Node 20.6+)

Core modules. These ship with Node, so there's nothing to install. You can import them with a node: prefix (import { readFile } from 'node:fs/promises';), which makes it obvious they're built in.

The Node.js toolbox: core modules fs for files, path for joining paths, http and https for servers and requests, events for EventEmitter, stream for chunked data, os, url and util for system and helpers, crypto for hashes and random values, child_process for running programs; npm commands npm init, npm install, npm install -D, npm ci, npm run test, npx, npm update and npm audit; semantic versioning where caret 1.4.2 allows minor and patch updates below 2.0.0, tilde 1.4.2 allows patch updates below 1.5.0, and 1.4.2 is exact

Core modules are built in. npm adds everything else, within the version ranges you allow.

import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
import { EventEmitter } from 'node:events';

const alerts = new EventEmitter();
alerts.on('error-line', (line) => console.error('Found:', line));

const rl = createInterface({ input: createReadStream('./app.log') });
for await (const line of rl) {          // one line at a time, constant memory
  if (line.includes('ERROR')) alerts.emit('error-line', line);
}

CommonJS and ES modules. Node supports both module systems, and you'll see both in real projects:

CommonJSES modules
Syntaxconst x = require('x'), module.exports = ...import x from 'x', export ...
When Node uses it.cjs files, or .js without "type": "module".mjs files, or .js with "type": "module" in package.json
LoadingSynchronous, at the point require runsStatic imports are resolved before the code runs; import() is async
Top-level awaitNoYes

Libraries and frameworks. The exam guide links to a list of popular Node.js frameworks. You don't need to know their APIs in depth, but you should match each to its purpose:

RequirementCommon choice
Minimal web server with routing and middlewareExpress
High-performance API with schema validationFastify
Large, structured TypeScript back end with dependency injectionNestJS
React app with server-side rendering and API routesNext.js
Real-time, two-way communication such as chat or live dashboardsSocket.IO (built on WebSockets)
Unit testingJest, Mocha with an assertion library such as Chai, or the built-in node:test

Package management. npm ships with Node and is the default package manager; Yarn and pnpm are popular alternatives that use the same registry. package.json describes your project: its name, version, scripts, dependencies (needed at runtime), devDependencies (needed only to build and test, such as Jest or ESLint) and fields like type. package-lock.json records the exact version of every installed package, including nested ones, and belongs in source control.

ScenarioMost fitting solution
CI must install exactly what was tested locallyCommit package-lock.json and run npm ci
A tool is only needed for development, such as a test runnernpm install -D jest (a devDependency)
Run a package's CLI once without installing it globallynpx package-name
Run a project task with the right local tools on the pathA scripts entry, run with npm run name (npm test and npm start are shortcuts)
Get bug fixes automatically but avoid breaking changesA caret range such as ^2.3.1 (npm's default)
Allow only patch updates for a sensitive dependencyA tilde range such as ~2.3.1
Check installed packages for known vulnerabilitiesnpm audit

Semantic versioning is MAJOR.MINOR.PATCH: breaking changes bump MAJOR, new backward-compatible features bump MINOR, and fixes bump PATCH. One edge case: for versions below 1.0.0, a caret is stricter, so ^0.4.2 only allows 0.4.x patch updates, because pre-1.0 minor releases are allowed to break things. npm install can update the lockfile within your ranges; npm ci deletes node_modules, installs exactly what the lockfile says, and fails if package.json and the lockfile disagree.

Key takeaway: match the requirement to the tool: streams for big data, npm ci with a lockfile for reproducible installs, -D for dev-only tools, and Express-style frameworks for web servers.

Practice questions: Server Side JavaScript

Question 1. A Node.js job must scan a 4 GB server log every night and count lines that contain TIMEOUT. The container has 512 MB of memory. Which implementation is the best fit?

  • A. fs.readFileSync('server.log', 'utf8').split('\n'), then filter the array
  • B. Read the file with fs.createReadStream() and process it line by line with readline
  • C. require('./server.log') so Node caches the file
  • D. Read the file with await readFile() from fs/promises, then use a regular expression on the whole string

Answer: B. A stream reads the file in small chunks, and readline turns those chunks into lines, so memory stays roughly constant no matter how big the file is. Why not the others: A and D both load the entire 4 GB file into memory, which can't fit, and A also blocks the event loop; require is for modules and JSON, not log files, and would still load everything (C).

Question 2. A team's build passes on a developer's laptop but fails in CI because CI installed a newer minor version of a dependency declared as "chart-lib": "^3.2.0". They want CI to install exactly the versions the developer tested. What should they do?

  • A. Change the range to ~3.2.0 and keep running npm install in CI
  • B. Commit package-lock.json and run npm ci in the CI pipeline
  • C. Run npm update before every CI build
  • D. Delete package-lock.json so npm always resolves fresh versions

Answer: B. The lockfile records the exact resolved version of every package, and npm ci installs exactly those versions (and fails if the lockfile and package.json disagree). That makes CI match the tested setup. Why not the others: a tilde range still allows new patch versions, so builds can still drift (A); npm update deliberately moves to newer versions within the range (C); deleting the lockfile guarantees drift (D).

Testing (7%)

One objective, and it's very specific:

  • With a block of code and the associated Unit Test, determine where the test is ineffective and modify it to make it more effective.

In other words, you'll read a function and its test, spot why the test would pass even when the code is wrong (or fail for the wrong reason), and choose the change that fixes it. The guide says testing is platform agnostic, but the snippets usually look like Jest, which is also what Salesforce uses for LWC.

Test anatomy. A good unit test follows arrange, act, assert: set up inputs and dependencies, call the unit once, then check a specific outcome. The exam focuses on unit tests, which check one function or component in isolation.

MatcherChecksCommon mistake
toBe(x)Identity with Object.is (same primitive value or same object)Using it for objects or arrays that are equal but not the same instance
toEqual(x)Recursive equality of valuesIgnores undefined properties; use toStrictEqual to catch them
toBeCloseTo(x, digits)Floating-point numbers within a precisionUsing toBe(0.3) for 0.1 + 0.2
toThrow(msgOrType)The function throwsCalling the function directly instead of wrapping it: expect(() => fn()).toThrow()
toBeTruthy()Any truthy valueToo weak to prove a specific result
resolves / rejectsThe outcome of a promiseForgetting to await or return the expectation
toHaveBeenCalledWith(...)A mock was called with specific argumentsChecking only toHaveBeenCalled()

Test doubles. Replace slow or unpredictable dependencies, never the unit under test. In Jest, jest.fn() creates a mock function that records its calls, jest.spyOn(obj, 'method') wraps an existing method, jest.mock('module') replaces a whole module, and jest.useFakeTimers() with jest.advanceTimersByTime(ms) makes timer code testable without waiting.

Turning a weak test into an effective one: ineffective test smells are no assertion or only toBeTruthy, only the happy path tested, async code not awaited or returned, the unit under test itself mocked, the expected value computed the same way as the code, and one test checking many unrelated things; effective habits are arrange, act, assert with specific values, edge cases such as empty, null, zero and boundaries, awaiting promises or using resolves and rejects, mocking dependencies like fetch rather than the unit, asserting errors with toThrow and the message, and one behavior per test with a clear name

Most exam testing questions are one of these smells. Name it, then pick the fix.

Here's a typical before and after. The function applies a 10% discount to orders over $100 and rejects negative totals:

export function applyDiscount(total) {
  if (total < 0) throw new RangeError('Total cannot be negative');
  return total > 100 ? Math.round(total * 90) / 100 : total;
}

// Ineffective: passes for almost any implementation
test('discount works', () => {
  expect(applyDiscount(200)).toBeTruthy();
});

// Effective: specific values, the boundary on both sides, and the error path
test('applies 10% above $100', () => {
  expect(applyDiscount(200)).toBe(180);
  expect(applyDiscount(100.01)).toBeCloseTo(90.01, 2);
});
test('no discount at exactly $100 or below', () => {
  expect(applyDiscount(100)).toBe(100);
  expect(applyDiscount(0)).toBe(0);
});
test('rejects negative totals', () => {
  expect(() => applyDiscount(-1)).toThrow(RangeError);
});

The weak test would still pass if the discount were 50%, if the boundary were >= instead of >, or if negative totals were silently accepted. The improved tests would catch all three. That's the question to ask on the exam: what bug could slip past this test?

Async tests. If a test starts a promise and doesn't wait for it, the test finishes first and passes no matter what the promise does. Make the test function async and await the call, return the promise, or use await expect(promise).resolves.toEqual(...). Adding expect.assertions(n) makes a test fail if the expected number of assertions never ran, which catches skipped then or catch blocks.

Coverage is not effectiveness. A test can execute every line (100% coverage) without asserting anything meaningful. Coverage tells you what code ran, not what behavior is proven.

Testing Lightning Web Components. LWC uses Jest through the @salesforce/sfdx-lwc-jest package. A test creates the component with createElement from lwc, appends it to document.body, then queries its shadow DOM with element.shadowRoot.querySelector(). After changing a property, await Promise.resolve() gives the component a microtask to re-render before you assert, which is the event loop showing up in a testing context. Apex calls and wire adapters are mocked.

Key takeaway: an effective test asserts specific outcomes, covers boundaries and error paths, waits for async work, and mocks only dependencies.

Practice questions: Testing

Question 1. This test passes, even though the expected name is wrong. What change makes the test effective?

export function fetchUser(id) {
  return fetch(`/api/users/${id}`).then((res) => res.json());
}

test('loads the user', () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({ json: () => Promise.resolve({ id: 1, name: 'Ana' }) }));
  fetchUser(1).then((user) => {
    expect(user.name).toBe('Bob');
  });
});
  • A. Replace the assertion with expect(user).toBeTruthy()
  • B. Replace toBe('Bob') with toEqual('Bob')
  • C. Move the fetch mock into an afterEach block
  • D. Make the test async and await fetchUser(1) before asserting (or return the promise chain)

Answer: D. The test never waits for the promise. Jest sees the synchronous test function return, marks it as passed, and the assertion runs later, after the test is over. Awaiting (or returning) the promise makes Jest wait, so the wrong expected value fails the test as it should. Adding expect.assertions(1) is a good extra safeguard. Why not the others: toEqual doesn't fix the timing, and for strings it behaves the same as toBe (B); a mock set up in afterEach would run after the test, so fetch wouldn't be mocked during it (C); toBeTruthy is weaker and still doesn't wait (A).

Deep dive: design decisions and code tracing

Two skills cut across every section: picking the right construct for a requirement, and tracing a snippet line by line without running it. Here's a reference for the first and a method for the second.

Design decisions at a glance.

DecisionChooseWhen
const, let or varconst by default, let when reassigningAvoid var in new code; know its rules for reading old code
Arrow or regular functionArrow for callbacks and transformsRegular functions or methods when you need your own this, arguments or new
Class or factory functionClass for many instances sharing methods, or for inheritanceA factory with closures for simple private state without this
for...of, forEach or mapmap to build a new array, for...of when you need await, break or continueforEach only for simple synchronous side effects
?? or ||?? when 0, '' or false are valid values|| when any falsy value should fall back
textContent or innerHTMLtextContent for any user-provided textinnerHTML only for trusted, sanitized markup
Promise.all or allSettledall when every result is requiredallSettled when partial success is useful
Sequential or parallel awaitParallel with Promise.all for independent callsSequential when each call needs the previous result

A five-step method for tracing code. When a question shows a snippet and asks what it logs, work through it in this order, on your scratch paper or whiteboard:

  1. Hoist. List every declaration in each scope. Function declarations are fully available from the top. var names exist from the top as undefined. let, const and class names exist but throw a ReferenceError until their line runs (the temporal dead zone).
  2. Resolve names. For each variable use, find the nearest enclosing scope that declares it. Watch for shadowing, where an inner declaration hides an outer one with the same name.
  3. Find this at each call site using the rules from the Objects section. Ignore where the function was written unless it's an arrow function.
  4. Split the timeline. Mark each line as synchronous, microtask (promise callbacks, code after await) or task (timers, events). Write the synchronous output first, then the microtasks in queue order, then the tasks.
  5. Track identity. For every assignment involving an object or array, ask whether it creates a new object or shares a reference, and whether the method you're calling mutates.

Hoisting in action. Each of these lines behaves differently, and the exam likes to put two of them side by side:

console.log(greet());      // 'hi': function declarations are fully hoisted
console.log(count);        // undefined: var is hoisted and initialized to undefined
console.log(total);        // ReferenceError: total is in the temporal dead zone
console.log(helper());     // never reached; helper would be undefined -> TypeError

function greet() { return 'hi'; }
var count = 5;
let total = 10;
var helper = function () { return 'help'; };

Note the difference in the last two errors: touching a let before its line is a ReferenceError, while calling a var function expression too early is a TypeError ("helper is not a function"), because the variable exists and holds undefined.

The loop closure classic. With var, there's one i shared by the whole loop, so every callback sees its final value. With let, each iteration gets a fresh binding:

for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));   // 3 3 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j));   // 0 1 2

var, let and const compared: var is function scoped, hoisted and set to undefined, redeclarable and reassignable, with one shared binding in a loop; let is block scoped, hoisted but in the temporal dead zone until declared, reassignable but not redeclarable, with a new binding per loop iteration; const is block scoped, must be initialized, cannot be reassigned, but object contents can still change; the scope chain shows a global scope containing a makeCounter function scope containing an inner closure that keeps count alive

Name lookups walk outward through enclosing scopes. Closures keep those scopes alive.

Control flow details that change output. A few more patterns that show up in execution-flow questions:

  • Short-circuiting skips evaluation. In user && user.save(), save() never runs if user is falsy. In a || b(), b() never runs if a is truthy.
  • Block-scoped declarations shadow outer ones. let x = 1; { let x = 2; } console.log(x); logs 1.
  • Strict mode changes plain calls. Inside classes and modules, a function called on its own has this === undefined, so reading this.anything throws a TypeError.

Worked example: a JavaScript order tracker, end to end

Let's put the outline together with a fictional project, the kind of small app a Salesforce or web developer actually builds.

The customer. Ridgeway Outfitters sells camping gear online. Its support team wants an order tracker page: search orders, see status cards grouped by status, cancel open orders, and get a warning when the shipping API is slow. A small Node.js service sits between the page and the warehouse system.

Shape the data (Variables, Types, and Collections). The warehouse API returns JSON with ISO date strings and money in cents. The developer parses it with a reviver that turns placedAt into a Date, keeps money as integer cents to avoid floating-point errors, and formats it only for display with Intl.NumberFormat. Orders are grouped by status with reduce.

const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
const orders = JSON.parse(payload, (k, v) => (k === 'placedAt' ? new Date(v) : v));
const byStatus = orders.reduce((acc, o) => {
  (acc[o.status] ??= []).push(o);
  return acc;
}, {});
const label = (o) => `${o.id} · ${money.format(o.totalCents / 100)}`;

Write a utility module (Objects, Functions, and Classes). Pure helpers live in orders.js with named exports, and the page imports only what it needs. An OrderStore class keeps the list in a private #orders field, exposes a get open() getter, and uses a static fromJSON() factory. Search input is wrapped in the debounce closure from earlier so the API isn't called on every keystroke.

Build the UI and events (Browser and Events). Cards are rendered with createElement and textContent, never innerHTML, because order notes contain customer-typed text. One delegated click listener on the card container handles every Cancel button with event.target.closest('button[data-order-id]'). When a card is selected, the component dispatches an orderselect custom event with the order ID in detail, so the rest of the page can react without tight coupling. The last search term goes in sessionStorage so a refresh keeps it, but a new tab starts fresh.

Load data asynchronously (Asynchronous Programming). The page loads orders, carrier status and return policies in parallel with Promise.allSettled, so a carrier outage still shows orders. Each fetch checks response.ok. A timeout uses AbortController plus setTimeout, and the slow-API warning appears if the request takes more than three seconds.

async function loadDashboard() {
  const results = await Promise.allSettled([
    getJSON('/api/orders'), getJSON('/api/carriers/status'), getJSON('/api/policies')
  ]);
  const [orders, carriers, policies] = results.map((r) =>
    r.status === 'fulfilled' ? r.value : null);
  render({ orders, carriers, policies });
  results.filter((r) => r.status === 'rejected')
         .forEach((r) => console.warn('Widget failed:', r.reason.message));
}

Add a Node.js service (Server Side JavaScript). An Express app exposes /api/orders, reads the warehouse key from process.env, and streams the nightly order export with fs.createReadStream() rather than loading it into memory. Jest is a devDependency, the lockfile is committed, and the CI pipeline runs npm ci then npm test.

Test and debug (Debugging and Error Handling, Testing). Unit tests cover label() with zero cents, large totals and missing lines; the grouping function with an empty array; and loadDashboard() with one mocked rejection, awaited properly. A rare NaN total turns out to come from a line item with a missing price, found with a conditional breakpoint instead of a dozen console.log calls.

Worked example for Ridgeway Outfitters order tracker in six steps: shape the data by parsing the order API JSON and grouping orders by status, write a utility module with named exports and closures, build the UI and events with delegation and a custom event, load data asynchronously with async and await and Promise.allSettled, add a Node.js service with environment variables, npm scripts and a lockfile, then test and debug with edge cases, breakpoints, console.table and error handling

Every exam section shows up in one small, realistic project.

Practice questions: worked example

Question 1. Ridgeway's tracker must turn the order list into an object such as { open: [...], shipped: [...] }. Which implementation does that correctly for any number of statuses?

  • A. orders.map((o) => ({ [o.status]: o }))
  • B. orders.reduce((acc, o) => { (acc[o.status] ??= []).push(o); return acc; }, {})
  • C. orders.reduce((acc, o) => { (acc[o.status] ??= []).push(o); return acc; })
  • D. orders.filter((o) => o.status)

Answer: B. reduce with an empty object as the initial value builds one accumulator. ??= creates the array for a status the first time it appears, and the callback returns the accumulator for the next item. (Newer runtimes also offer Object.groupBy(), which is newer than the exam.) Why not the others: map returns an array of one-key objects, not one grouped object (A); without an initial value, reduce uses the first order as the accumulator, so statuses get added as properties of that order object (C); filter only removes orders without a status (D).

Question 2. Out of about 400 orders, a handful show $NaN as the total. The developer wants the debugger to pause inside calculateTotal() only for those orders, without editing or redeploying code. What should they use?

  • A. console.log(total) on every line of the function
  • B. A debugger; statement at the top of calculateTotal()
  • C. A conditional breakpoint on the return line with the condition Number.isNaN(total)
  • D. The Network panel, filtered to the orders API

Answer: C. A conditional breakpoint pauses only when its expression is true, so the developer stops exactly on the broken orders and can inspect the line items in the Scope panel. It's set in DevTools, so no code changes are needed. Why not the others: debugger; requires a code change and pauses on every one of the 400 calls (B); scattering console.log also means editing code and produces noise (A); the Network panel shows the raw response, not where the calculation goes wrong (D).

Question 3. When a support agent selects an order card, the tracker must notify a listener registered on document, and include the order ID. The card elements are plain DOM elements (no shadow DOM). Which code should the card dispatch?

  • A. document.orderselect(id)
  • B. card.dispatchEvent(new Event('orderselect', { detail: { id } }))
  • C. card.dispatchEvent(new CustomEvent('orderselect', { detail: { id } }))
  • D. card.dispatchEvent(new CustomEvent('orderselect', { detail: { id }, bubbles: true }))

Answer: D. CustomEvent carries data in detail, and bubbles: true lets the event travel up from the card to document, where the listener is registered. Inside an LWC, you'd also need composed: true to cross the shadow boundary. Why not the others: a plain Event ignores the detail option, so the ID is lost (B); without bubbles: true the event stays on the card and never reaches document (C); document has no such method, and calling one directly would couple the card to the page (A).

Hands-on checklist

Do these in a browser console, a small HTML file or Node.js. Each one maps to likely exam questions.

  1. Log typeof for every type, including null, a function, an array and NaN. Then test the eight falsy values with Boolean().
  2. Sort [10, 9, 1, 100] with and without a compare function. Confirm that sort() mutates and toSorted() doesn't.
  3. JSON.stringify an object with undefined, a function, a Date, NaN and a Map. Then parse it back with a reviver.
  4. Build a class hierarchy with a private field, a static method, a getter and a subclass. Break it by touching this before super().
  5. Create two ES modules with named and default exports, then load them in the browser with <script type="module"> and in Node with "type": "module".
  6. Build a nested HTML list with capture and bubble listeners on each level. Click and log the order, then add stopPropagation() at different levels.
  7. Call a public API with fetch, request a URL that returns 404, and handle it with a response.ok check. Add an AbortController timeout.
  8. Write five event-loop puzzles mixing setTimeout, Promise.then, queueMicrotask and await. Predict the output before running them.
  9. Set a conditional breakpoint, a logpoint and an event listener breakpoint in DevTools. Try console.table, console.time and console.assert.
  10. Write a weak Jest test, then improve it with exact values, boundaries, an error case and an awaited async case.

Common exam traps

  • typeof null is "object". And typeof an array is "object" too; use Array.isArray().
  • + concatenates if either side is a string. Every other arithmetic operator converts to numbers.
  • sort() mutates and sorts as strings by default. So do reverse() and splice(); slice() doesn't.
  • reduce on an empty array without an initial value throws.
  • JSON.stringify drops undefined and functions and turns NaN into null; dates become strings.
  • const doesn't freeze objects, and Object.freeze is shallow.
  • Arrow functions don't have their own this. Passing a regular method as a callback loses this.
  • Derived constructors must call super() before using this.
  • event.target isn't always the element with the listener. That's currentTarget.
  • preventDefault() doesn't stop propagation, and stopPropagation() doesn't cancel the default action.
  • fetch resolves on HTTP errors. Check response.ok.
  • A try around setTimeout doesn't catch errors thrown in the callback.
  • The promise executor runs synchronously; then callbacks never do.
  • Microtasks run before timers, even a setTimeout with a 0 ms delay.
  • forEach doesn't wait for async callbacks. Use for...of or Promise.all.
  • A test that doesn't await its promise passes no matter what.

Flashcard terms

  • Primitive: an immutable value of type string, number, bigint, boolean, undefined, null or symbol.
  • Reference: what an object variable holds; copying it shares the same object.
  • Type coercion: implicit or explicit conversion between types, such as '5' * 2 giving 10.
  • Falsy: one of eight values treated as false: false, 0, -0, 0n, '', null, undefined, NaN.
  • Nullish coalescing (??): returns the right side only when the left is null or undefined.
  • Temporal dead zone: the period before a let, const or class declaration runs, when accessing it throws a ReferenceError.
  • Hoisting: declarations being processed before code runs; function declarations are fully usable early.
  • Closure: a function plus the variables it captured from the scope where it was created.
  • Prototype chain: the linked objects JavaScript searches when a property isn't found on an object.
  • Private field (#x): a class field accessible only inside the class body.
  • Live binding: an ES module import that reflects the exporter's current value and can't be reassigned by the importer.
  • Event propagation: the capture, target and bubble phases an event travels through.
  • Event delegation: handling child events with one listener on a common ancestor.
  • CustomEvent: a developer-defined event that carries data in detail.
  • Conditional breakpoint: a breakpoint that pauses only when an expression is true.
  • Promise: an object for a future value that settles once, as fulfilled or rejected.
  • Microtask: a high-priority callback, such as a promise reaction, run before the next task.
  • Task (macrotask): a callback such as a timer or UI event, run one per event loop turn.
  • Event loop: the mechanism that runs all microtasks, then one task, whenever the call stack is empty.
  • AggregateError: an error holding several errors, thrown by Promise.any when every promise rejects.
  • Stream: a way to process data in chunks instead of loading it all into memory.
  • package-lock.json: the record of exact installed versions, used by npm ci.
  • Caret range (^): allows minor and patch updates within the same major version.
  • Test double: a stand-in for a dependency, such as a mock, stub, spy or fake.
  • Boundary value: a test input at, just below or just above a limit.

Mixed practice exam: 4 more questions

Question 1. A developer schedules three retry messages in a loop. What's logged?

for (var attempt = 0; attempt < 3; attempt++) {
  setTimeout(() => console.log(`Retry ${attempt}`), 100);
}
  • A. Retry undefined three times
  • B. Retry 0, Retry 1, Retry 2
  • C. Retry 1, Retry 2, Retry 3
  • D. Retry 3, Retry 3, Retry 3

Answer: D. var is function scoped, so the loop has a single attempt variable. The callbacks run after the loop finishes, when attempt is 3. Changing var to let creates a new binding per iteration and logs 0, 1, 2. Why not the others: B is the output with let, not var; C is off by one and still assumes separate bindings; the variable is defined when the callbacks run, so it isn't undefined (A).

Question 2. In an ES module, a developer destructures a method from an object to pass it around. What happens on the last line?

const product = {
  name: 'Tent',
  weight: 2,
  describe() { return `${this.name} (${this.weight} kg)`; }
};
const { describe } = product;
console.log(describe());
  • A. It logs Tent (2 kg)
  • B. It logs undefined (undefined kg)
  • C. It throws a TypeError
  • D. It throws a ReferenceError

Answer: C. describe is called on its own, so this comes from a plain call. ES modules are strict mode, which makes this undefined, and reading this.name on undefined throws a TypeError. Calling product.describe() or using product.describe.bind(product) fixes it. Why not the others: A would require this to be product, which only happens with a method call or bind; B is what a sloppy-mode script might show, because this would be the global object there, not in a module; the variable describe exists, so there's no ReferenceError (D).

Question 3. A long support form must keep a half-finished draft if the agent accidentally closes the browser and comes back the next day on the same computer. The draft shouldn't be sent to the server until the agent submits. Where should the page store it?

  • A. sessionStorage, with JSON.stringify on save and JSON.parse on load
  • B. localStorage, with JSON.stringify on save and JSON.parse on load
  • C. A cookie, so it's available on every request
  • D. A module-level variable in the page's JavaScript

Answer: B. localStorage persists for the origin until it's cleared, survives closing the browser, and isn't sent to the server. It only stores strings, so serialize the draft with JSON.stringify and parse it on load. Why not the others: sessionStorage is cleared when the tab closes (A); cookies are sent with every request and are limited to about 4 KB, which conflicts with the requirement (C); a variable disappears when the page unloads (D).

Question 4. What's the console output?

console.log('start');
const p = new Promise((resolve) => {
  console.log('executor');
  resolve('done');
});
p.then((value) => console.log(value));
console.log('end');
  • A. start, executor, end, done
  • B. start, end, executor, done
  • C. start, executor, done, end
  • D. executor, start, end, done

Answer: A. The function passed to new Promise runs synchronously during construction, so executor logs right after start. Calling resolve doesn't run then callbacks immediately; they're queued as microtasks, so done logs after the synchronous end. Why not the others: B assumes the executor is asynchronous; C assumes then runs synchronously once the promise is resolved; D ignores that start is logged before the promise is created.

Quick-reference cheat sheet

TopicRemember
Format60 scored + up to 5 unscored, 105 min, 65% to pass, US$200, retake US$100
Prerequisite and superbadgeNo prerequisite; superbadge not required since October 1, 2025
Largest sectionsObjects, Functions, and Classes 25%; Variables, Types, and Collections 23%
Types7 primitives plus object; typeof null is "object"; Array.isArray() for arrays
Coercion+ with a string concatenates; - * / convert to numbers; === never coerces
Falsyfalse, 0, -0, 0n, '', null, undefined, NaN
Arrayssort, splice, reverse, push mutate; map, filter, slice, toSorted don't
JSONDrops undefined and functions; NaN to null; dates to ISO strings; BigInt throws
thisCall site decides; arrows inherit; bind returns a new function
Classessuper() before this; #private; static on the class; methods on the prototype
ModulesBraces for named imports; default imports take any name; evaluated once; live bindings
EventsCapture, target, bubble; target vs currentTarget; delegate with closest()
fetchRejects only on network failure; check response.ok; cancel with AbortController
Errorsfinally always runs; throw Error objects; try can't catch later callbacks
Event loopSync, then all microtasks, then one task; promises before setTimeout
NodeStreams for big files; npm ci with a lockfile; -D for dev tools; ^ minor, ~ patch
TestingSpecific assertions, boundaries, error paths, await async, mock dependencies only

Salesforce JavaScript Developer quick reference tiles: format of 60 scored questions plus up to 5 unscored in 105 minutes with 65% to pass for US$200, the biggest sections Objects 25% and Variables 23%, the eight falsy values, use triple equals, this is decided at the call site, the event loop runs sync code then microtasks then tasks, promise combinators, npm caret and tilde ranges, and testing with specific assertions

Review this the night before the exam.

Frequently asked questions

Is JavaScript Developer the same as JavaScript Developer I? Yes. Salesforce now calls the credential Salesforce Certified JavaScript Developer, but the exam guide, outline and weights are the same ones you'll find in material that says JavaScript Developer I, and trail URLs still use the old name.

Do I still need the Lightning Web Components Specialist superbadge? No. Salesforce retired that requirement on October 1, 2025. Passing the proctored exam earns the certification.

How hard is the exam? The passing score is 65%, but most questions hinge on one precise detail, such as whether a method mutates or when a callback runs. Developers who write JavaScript daily find it fair if they practice output-prediction questions. Developers who mostly write Apex usually need focused time on coercion, this, async and Node.

How much Salesforce is on the exam? Very little. The exam guide calls it an industry-standard JavaScript certification, and the outline is core JavaScript, the browser, Node.js and testing. Knowing LWC helps because it's modern JavaScript, but you won't be asked about Apex or org configuration.

Which JavaScript version should I study? Study modern JavaScript through roughly ES2022: let and const, arrow functions, classes with private fields, modules, promises, async and await, optional chaining and nullish coalescing. The exam aligns to Summer '22, so newer additions such as toSorted() or Object.groupBy() are nice to know but unlikely to be tested.

How do I keep the certification? Complete the annual JavaScript Developer certification maintenance badge on Trailhead before the due date, or the certification expires.

Should I take this or Platform Developer first? If you write Apex and work in orgs every day, start with Platform Developer. If you come from web development or spend most of your time in LWC, start here. The certification study guides hub can help you plan the rest of the path.

Want to go deeper on automation? JavaScript and Flow meet more often than people expect: screen flows host Lightning Web Components, and a lot of what developers once coded is now built declaratively. Knowing where Flow ends and code begins makes you a better Salesforce developer. My Salesforce Flows course walks through record-triggered, screen, scheduled and platform event flows with real-world challenges.

Hope this helps!

Best,

Nick