Original notes for JavaScript360.org — topic order inspired by Eloquent JavaScript (4th ed.) by Marijn Haverbeke. See the full attribution note.
Chapter 18: HTTP and Forms
Estimated time: ~30 minutes · Chapter type: Concept
What you'll learn
- The shape of an HTTP request and response
- Making network requests with
fetch, including error handling - Reading data out of HTML forms
- Handling form submission without a full page reload
HTTP, briefly
HTTP (HyperText Transfer Protocol) is the request/response protocol the web runs on. Every request has a method describing the intent, a URL identifying the resource, optional headers (metadata), and an optional body (data being sent); every response has a status code summarizing the outcome, its own headers, and usually a body:
| Method | Typical meaning |
|---|---|
GET | Fetch a resource, no side effects |
POST | Create something new, or submit data |
PUT | Replace a resource entirely |
PATCH | Partially update a resource |
DELETE | Remove a resource |
| Status range | Meaning |
|---|---|
200–299 | Success |
300–399 | Redirection |
400–499 | Client error (you did something wrong — bad input, not authenticated) |
500–599 | Server error (something broke on their end) |
Two specific codes worth knowing by number because you'll see them constantly: 404 (not found) and 401/403 (not authenticated / not authorized).
Making requests with fetch
fetch is the standard, Promise-based way to make an HTTP request from JavaScript (in both the browser and Node.js):
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json(); // also returns a Promise — parses the body as JSON
}
A crucial detail that trips people up: fetch only rejects on a network failure (no connection, DNS failure, and similar) — a 404 or 500 response is still a "successful" fetch as far as the Promise is concerned. You must check response.ok (or response.status) yourself to detect an HTTP-level error, as the example above does.
Sending data (a POST, for instance) means setting the method, headers, and a body explicitly:
async function createTask(title) {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
if (!response.ok) throw new Error(`Failed to create task: ${response.status}`);
return response.json();
}
The Content-Type: application/json header tells the server how to interpret the body — without it, many servers won't correctly parse the JSON you sent.
Reading form data
An HTML form collects user input across multiple fields:
<form id="signup">
<input name="email" type="email" required />
<input name="age" type="number" />
<button type="submit">Sign up</button>
</form>
FormData reads every named field from a form element in one step, without you manually querying each input individually:
const form = document.getElementById('signup');
form.addEventListener('submit', async (event) => {
event.preventDefault(); // stop the browser's default full-page submission (Chapter 15)
const data = new FormData(form);
const email = data.get('email');
const age = Number(data.get('age'));
await fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, age }),
});
});
event.preventDefault() is essential here: without it, submitting the form triggers the browser's default behavior — a full page navigation to the form's action URL — which would throw away your JavaScript's control of the flow entirely.
Basic client-side validation
The browser gives you some validation for free through HTML attributes (required, type="email", min/max), but real applications almost always add their own checks too, both for a better error message and because HTML validation alone isn't sufficient (the server must always re-validate — never trust data purely because the client-side check passed):
function validateSignup({ email, age }) {
const errors = [];
if (!email.includes('@')) errors.push('Enter a valid email address.');
if (!Number.isInteger(age) || age < 13) errors.push('Age must be a whole number, 13 or older.');
return errors;
}
Putting it together: showing errors without losing the form
form.addEventListener('submit', async (event) => {
event.preventDefault();
const data = new FormData(form);
const values = { email: data.get('email'), age: Number(data.get('age')) };
const errors = validateSignup(values);
if (errors.length > 0) {
document.querySelector('#errors').textContent = errors.join(' ');
return; // stop here — don't send an invalid request
}
try {
await createTask(values); // reusing the fetch helper from earlier
form.reset();
} catch (err) {
document.querySelector('#errors').textContent = 'Something went wrong. Please try again.';
}
});
Key takeaways
- HTTP requests have a method, URL, headers, and optional body; responses have a status code you must check explicitly —
fetchdoesn't reject on a404or500. - Always set
Content-Type: application/json(andJSON.stringifythe body) when sending JSON withfetch. FormDatareads all of a form's fields at once;event.preventDefault()stops the browser's default full-page submission so you can handle it in JavaScript.- Validate on the client for a fast, friendly experience — but never skip validating again on the server, since client-side checks can always be bypassed.
Try it yourself
- Write a
fetch-based functiondeleteTask(id)that sends aDELETErequest and throws a descriptive error if the response isn'tok. - Add a required
passwordfield to the signup form example, and extendvalidateSignupto require at least 8 characters. - Explain, in your own words, why checking
response.okis necessary even though you alreadyawaited thefetchcall without it throwing.
Hints
- Call
fetchwith the task's URL (built with a template literal) and{method: 'DELETE'}as the second argument, then reuse the same "checkresponse.ok, throw if not" pattern asloadUser. data.get('password'), andif (password.length < 8) errors.push(...).fetch's Promise only reflects whether the network round-trip itself succeeded — the server can still respond with a 4xx/5xx "successfully," which is a normal, awaited response, not a rejection.
Summary
fetch and forms are how the vast majority of real applications get data on and off the page — everything from a login screen to an infinite-scrolling feed is some variation of these patterns. Our next project brings the DOM, canvas, and event handling together into one interactive tool; after that, we leave the browser behind entirely for JavaScript's other home: the server.