JavaScript Basics for Testers

JavaScript is the programming language browsers run inside web pages. HTML gives a page its structure, CSS its appearance and JavaScript its behaviour. When you click "Add to cart" and the cart badge changes from 2 to 3 without a reload, a JavaScript function ran because of your click, sent a request to the server, waited for the answer and updated the page.

To test web applications you do not need to write JavaScript. You need to read a few lines of it, follow what happens between a click and the result, and read the errors the browser reports when a script fails.

Why it matters

In a modern web application most of what users do runs through JavaScript, and when it fails the page usually still looks fine: a button does nothing, a spinner keeps turning, a price shows NaN. Often the only evidence is a red message in the browser console, which users never see. A tester who never opens the console misses these failures, or files reports such as "the button doesn't work" that a developer cannot act on.

JavaScript also makes timing part of the behaviour: many defects appear only on a slow network, after an error response or after a second click.

Variables, values and functions

A variable is a named place for a value: const total = 150; cannot be reassigned, let qty = 1; can. Values have types: strings ('150'), numbers (150), booleans (true), objects ({ price: 25 }), arrays ([3, 5]), and undefined (no value set) or null (empty on purpose). A function is a named block of steps that runs when something calls it; reading one often shows which cases the developer forgot, such as a cart item without a product in the example below.

  • '5' + 1 gives '51': when either side is a string, + joins instead of adding, and a value read from a text field stays a string until the code converts it. A cart can show 51 items instead of 6.
  • 'abc' * 2 gives NaN (not a number), and any calculation with NaN gives NaN, which can end up on screen as a price.
  • A missing value used as text appears as the word "undefined" or as an empty label.
// cart.js: the kind of function you will read in a page
function lineTotal(item) {
  const price = item.product.price;  // a number, e.g. 25
  const qty = Number(item.qty);      // the field gives a string, e.g. '2'
  return price * qty;
}

lineTotal({ product: { price: 25 }, qty: '2' });  // 50
lineTotal({ product: undefined, qty: '2' });      // throws a TypeError

Events and asynchronous code

Most page code waits for events. The script attaches a handler to an element, for example button.addEventListener('click', applyCoupon), and the browser calls it each time the event happens. Common events are click, input, change, submit and keydown. A handler runs only for its own events: a check that listens for key presses never runs when a value is pasted with the mouse or filled in by autofill.

Talking to a server takes time, so that code is asynchronous. fetch() sends an HTTP request and at once returns a promise, an object standing for a result that is not there yet. A promise is pending until it is fulfilled with a value or rejected with an error; meanwhile, code that uses await pauses inside its async function and the rest of the page stays usable.

When the answer arrives, the promise is fulfilled with a response object, usually named res: res.ok is true only for statuses 200 to 299, and await res.json() reads the body as JSON, the text format most APIs use for data, written much like a JavaScript object. The fetch() promise is rejected only when the request itself fails, for example on a network error. A 404 or 500 response still fulfils it, so the code must check res.ok.

The same "Apply coupon" code with two answers: a 200 updates the page, while a 422 also fulfils the fetch() promise, the unchecked body throws a TypeError and the spinner keeps turning.
  • Valid coupon: 200 OK

    • User
    • Page script JavaScript in the browser
    • Server
    1. 1 From User To Page script: Clicks "Apply coupon" The click event calls the handler
    2. 2 Page script internally: Shows the spinner
    3. 3 From Page script To Server: Sends the request fetch() returns a pending promise at once; the page stays usable POST /api/coupons/apply
    4. 4 From Server To Page script: 200 OK reply JSON body with the discount
    5. 5 Page script internally: Promise fulfilled: reads data.discount.percent The code never checks res.ok, but this body has a discount
    6. 6 From Page script To User: New total, spinner hidden reply

    Outcome: It works, so a test with only valid coupons passes and the defect stays hidden. Pass

  • Expired coupon: 422

    • User
    • Page script JavaScript in the browser
    • Server
    1. 1 From User To Page script: Clicks "Apply coupon" The handler shows the spinner
    2. 2 From Page script To Server: Sends the request POST /api/coupons/apply
    3. 3 From Server To Page script: 422: coupon refused reply {"error":"COUPON_EXPIRED"}
    4. 4 Page script internally: Promise still fulfilled: a 422 is a response With no res.ok check, the code reads data.discount.percent
    5. 5 Page script internally: TypeError thrown, nothing catches it The function stops there; the line that hides the spinner never runs Fail
    6. 6 From Page script To User: Spinner keeps turning, no message reply The only trace is "Uncaught (in promise)" in the console

    Outcome: The user is stuck; the console error and the 422 response are the evidence for the bug report. Fail

The fetch() promise is rejected only when the request itself fails, for example on a network error; an error status such as 422 or 500 still fulfils it, so the code must check res.ok.

  • The user can act while a request is pending: click again, edit the field, leave the page. Check for duplicate requests and for a waiting state.
  • Answers can arrive out of order: in a search box, a slow answer for "ri" can overwrite the newer one for "riyadh".
  • Test at least success, an error status and no answer at all (offline or a timeout); a slow answer shows the waiting state.

Try it: API Status Code Simulator Try it: Mock Error Response Generator

How JavaScript breaks a page

When code throws an exception and nothing catches it, the browser abandons the code that was running, that function and every function that called it, and reports the error in the console. Other handlers and scripts keep running, so the rest of the page usually works and only the feature that hit the error goes quiet. In ISTQB terms you observe a failure, caused by a defect that a person introduced through an error (a mistake); JavaScript simply calls its exceptions errors. Typical failures:

  • A button does nothing: its handler threw before it changed anything.
  • A spinner never stops or a button stays disabled: the line that resets them comes after the one that failed.
  • Strange values, often with no error at all, because the code ran to the end with the wrong data: NaN, undefined, [object Object], 51 instead of 6.
  • A whole widget is missing: its script file did not load or has a syntax error, so none of its code ran.
  • It fails only for some data: an empty list, a missing optional field, a deleted product.

Reading a console error

Open the developer tools (F12 in most desktop browsers) and choose the Console panel; the DevTools lesson covers the other panels. Clear it, repeat the action and read only the new red errors. Below is what lineTotal from the first section reports, in Chrome's wording, when a cart item has no product.

  • Type: TypeError (an operation on the wrong kind of value, often undefined), ReferenceError (an unknown name), SyntaxError (code or JSON that cannot be parsed), RangeError (a value out of range).
  • Message: here, reading price of undefined, so item.product was missing. Ask which data caused it, for example a product removed from the catalogue. Wording differs between browsers; the type does not.
  • Location: file, line and column, cart.js:58:30.
  • Stack trace: where the error was thrown on top, each caller below. Read it bottom-up for the story (a button click ran an unnamed handler, which called renderCart, then lineTotal). Uncaught means nothing caught it; (in promise) means a rejected promise.
  • Live sites usually minify their code, often into one long line (app.3f9c1.js:1:20431); source maps lead developers back to the original, and a formatter makes it readable for you.
Uncaught TypeError: Cannot read properties of undefined (reading 'price')
    at lineTotal (cart.js:58:30)
    at renderCart (cart.js:31:17)
    at HTMLButtonElement.<anonymous> (cart.js:12:5)

Try it: JavaScript Formatter

Worked example: the "Apply coupon" button that never finishes

In exploratory testing of the checkout, the expired coupon WELCOME10 leaves the spinner turning forever; a valid coupon works. With the console cleared, the tester repeats the steps and collects the evidence below.

  • The server refused the coupon correctly, with 422 (Unprocessable Content: the request was well formed, but the server would not process its content) and an error code.
  • The fetch() promise was still fulfilled, and the code never checks res.ok. The error body has no discount, so line 42 throws a TypeError, and line 43, which hides the spinner, never runs.
  • Report: "Expired coupon leaves the spinner running and shows no message", with steps, expected result (spinner removed, an expired-coupon message), actual result, the console text and the request and response. Then test the other outcomes: unknown code, server error, slow answer, no network, double click.
Console
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'percent')
    at applyCoupon (checkout.js:42:44)

Network
POST /api/coupons/apply  ->  422   {"error":"COUPON_EXPIRED"}

checkout.js
38  async function applyCoupon() {
39    spinner.hidden = false;
40    const res = await fetch(url, options);
41    const data = await res.json();
42    total.textContent = format(data.discount.percent);
43    spinner.hidden = true;
44  }

Try it: Mock API Response Generator

Common mistakes

  • Testing with the console closed and reporting "the button doesn't work" without the error text.
  • Pasting only the first line of an error, or a screenshot of it: copy the type, the message and the stack trace as text.
  • Testing only fast, successful responses, when error statuses, slow answers, offline mode and double clicks are where asynchronous code breaks.
  • Filing every console message as a defect: some come from browser extensions or third-party scripts, so check the file first.
  • Using the console error as the bug title instead of describing the failure the user sees.

The QA perspective

Keep the console open while you test a web feature, clear it before each scenario, and investigate every new red error even when the screen looks right. Record the action, the full text and the request just before it; with the file and line, a developer can often find the defect quickly.

In reviews, ask for every action that calls the server which responses are possible and what the page should do for each: success, validation error, server error, timeout, no network, a second click. The answers become your test conditions, and the same timing questions return later as waits in UI automation.

Practice exercise

Read the error behind a "Load more" button

Your task

On the order history page, "Load more" shows only a spinner when it loads page 3; pages 1 and 2 work. Analyse the evidence below as you would before writing a bug report.

Material
Console (Chrome)
Uncaught (in promise) SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON

Network
GET /api/orders?page=3  ->  502 Bad Gateway   Content-Type: text/html

orders.js
12  async function loadMore(page) {
13    spinner.hidden = false;
14    const res = await fetch('/api/orders?page=' + page);
15    const orders = await res.json();
16    renderOrders(orders);
17    spinner.hidden = true;
18  }
Steps
  1. Name the error type and the line that must have thrown it.
  2. Explain why the message mentions < and why the fetch() promise was not rejected.
  3. Describe what the user sees and which lines never run.
  4. Decide what to report and to whom, and list four further tests.
Show the model answer
Model answer

SyntaxError: something could not be parsed. The only line that parses is 15, res.json(); Uncaught (in promise) confirms a rejected promise that nothing handled.

A 502 Bad Gateway comes from a gateway or proxy, a server in front of the API, when it gets an invalid answer from the server behind it. The body is that gateway's HTML error page (Content-Type: text/html), which starts with <!DOCTYPE, so JSON parsing fails at <. A 502 is still a response, so the fetch() promise was fulfilled, and the code never checks res.ok.

A spinner that never stops and no message. Lines 16 and 17 never run: no orders are added and the spinner is never hidden.

Two reports: to the back-end or operations team, GET /api/orders?page=3 returns 502, with the time of the request; to the front-end developer, a failed load shows no message and leaves the spinner, where a clear message and a usable button are expected. Further tests, for example: 500 and 504 responses, a slow response, offline mode, an empty last page, a double click on "Load more".

Frequently asked questions

Do I need to learn to program in JavaScript to test web applications?

Not to start. Reading variables, functions, events and promises and understanding console errors is enough for manual web testing. To automate browser tests with Cypress you write JavaScript or TypeScript, and Playwright Test, Playwright's own test runner, uses them too (Playwright also has Python, Java and .NET versions); this lesson is the foundation for those lessons.

Is JavaScript the same as Java?

No. They are two languages with different designs; the similar name is historical. JavaScript is standardised as ECMAScript in ECMA-262, and TypeScript is JavaScript with type annotations that is compiled to JavaScript before the browser runs it.

Is every red message in the console a bug?

Not necessarily. Red marks errors, including failed network requests, and some come from browser extensions or third-party scripts such as analytics. It is a bug when it comes from the product's own code or requests and affects behaviour, or when the team's rule is a clean console. When unsure, note the file and the action and ask the developer.

References

  • ECMAScript® Language Specification (ECMA-262)Ecma International / TC39The JavaScript language standard, including the RegExp grammar and semantics, JSON.parse/JSON.stringify and the Date object.
  • Fetch StandardWHATWGDefines CORS: preflight requests, the Access-Control-* headers and which responses a browser may expose to a page.
  • ISTQB GlossaryISTQBThe standard definitions of testing terms — test case, regression testing, severity, priority, boundary value analysis and the rest.
  • JavaScript — MDNMDN Web Docs (Mozilla)MDN's JavaScript guide and reference: the language's syntax, types, objects and built-ins as browsers implement them.

Practise with Mutqan tools

Beautify JavaScript and TypeScript code with consistent indentation and spacing.

Formatting

Get a public endpoint that returns any HTTP status code, with optional delay and custom JSON body, to test client error handling.

API Testing

Generate structured error responses (validation, auth, not found, rate limit, server error) in popular formats.

Mocking

Generate complete mock API responses (status, headers, JSON body) for success, list, detail, error and empty cases.

Mocking

Validate JSON syntax and get precise error locations with helpful fix suggestions.

JSON & Data

Go deeper

Your progress on this topic

Part of the QA Roadmap