The weather card looks ready: a city name, a temperature field, and one call to `fetch()`. Then the browser shows a CORS error, a 404 appears in the Network panel, or the code prints `undefined` even though the request “worked.” The beginner’s instinct is to rewrite the URL. The faster approach is to identify which layer failed.
An API is a contract between software systems. The client sends a request with a method, URL, headers, parameters, and sometimes a body. The server returns a status, headers, and a body that may be JSON. Learning APIs means learning to inspect that contract when reality differs from the example.
The broken version hides four different failures
fetch("https://api.open-meteo.com/v1/forecast")
.then(response => response.json())
.then(data => {
document.querySelector("#temperature").textContent =
data.current_weather.temperature;
});This snippet can fail in several unrelated ways. The URL may omit required query parameters. The server may return a 400 or 404 response that still reaches the next `.then()`. The body may not contain `current_weather`. The browser may block JavaScript from reading a cross-origin response because the server did not allow the page’s origin.
These are not interchangeable “API errors.” They belong to different layers:
| Layer | Question | Evidence to inspect |
|---|---|---|
| Request construction | Did the client send the intended URL, method, parameters, and headers? | Request URL, method, query string, request headers. |
| Network | Did a response arrive at all? | Console error, Network panel, timeout, DNS or connection details. |
| HTTP status | Did the server accept the request? | Status such as 200, 400, 401, 404, 429, or 500. |
| Representation | Does the body match the format and fields the code expects? | Response headers, raw body, JSON shape. |
| Browser policy | Is the page allowed to read a response from another origin? | CORS headers, preflight request, browser console. |
This layered view connects directly to our guides on HTTP and HTTPS and frontend versus backend. The browser is the client. The API server is another system. The network and the browser’s security rules sit between them.
Before: trusting `fetch()` too much
fetch("https://api.open-meteo.com/v1/forecast")
.then(response => response.json())
.then(data => showTemperature(data.current_weather.temperature));The first correction is not a new library. It is an explicit contract. The Open-Meteo forecast endpoint needs location and weather parameters in the query string. The service’s official documentation describes the endpoint and its required options. Build the URL from named parameters so the request is readable and testable.
const params = new URLSearchParams({
latitude: "52.52",
longitude: "13.41",
current: "temperature_2m",
});
const url = `https://api.open-meteo.com/v1/forecast?${params}`;`URLSearchParams` also prevents hand-written query strings from becoming a punctuation puzzle. If you change the location, you can inspect the resulting URL and compare it with the API documentation.
After: check the HTTP result before reading JSON
A common beginner assumption is that `fetch()` rejects whenever the server returns a failure status. It does not. A response with status 404 or 500 can still resolve the Promise because a server answered the request. The MDN reference for Response.ok defines it as true only for status codes in the 200–299 range in the Fetch response documentation.
async function getForecast() {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Forecast request failed: ${response.status}`);
}
return response.json();
}Now a 400, 404, or 500 becomes an explicit failure before the code treats the body as a successful forecast. The status code does not explain everything, but it prevents one class of false success.
| Status family | Useful beginner interpretation | What to inspect next |
|---|---|---|
| 2xx | The server reports success. | Validate the body and the fields your UI needs. |
| 3xx | The request involves redirection or cache behavior. | Check the final URL and client/server redirect rules. |
| 4xx | The request or authorization is not acceptable to the server. | URL, parameters, method, credentials, permissions, and rate limits. |
| 5xx | The server reports an error while handling the request. | Server status, retry policy, provider documentation, and logs if available. |
The status is evidence, not a complete diagnosis. A 401 suggests authentication is missing or invalid; a 429 suggests the client is being rate-limited; a 404 may mean the endpoint or resource path is wrong. Read the API’s documentation instead of guessing what each provider means.
Before: assuming the JSON shape
const data = await getForecast();
showTemperature(data.current_weather.temperature);Even after a successful status, the body can be different from the code’s assumption. An API may return an error object, a renamed field, a nested structure, or an empty result. Print or inspect a sanitized response before binding it to the page.
const data = await getForecast();
console.log(data);
if (typeof data.current !== "object" || data.current === null) {
throw new Error("Forecast response has no current data");
}
const temperature = data.current.temperature_2m;
if (typeof temperature !== "number") {
throw new Error("Forecast temperature is missing or invalid");
}
showTemperature(temperature);This is not a demand to manually validate every property forever. It is a learning and debugging technique. Once you know the real response contract, you can use a schema validator, TypeScript type, or focused helper. The important habit is refusing to treat a guessed shape as a fact.
JSON is a text representation, not a JavaScript object already living in memory. Calling response.json() reads the body and parses it. That operation can fail if the response is empty, truncated, or not JSON. Our guide to JavaScript objects helps with the language side; the API documentation defines the server’s actual fields.
The CORS wall is a browser rule, not a broken URL
Now imagine the URL works when pasted into a browser tab, but the page’s JavaScript reports a CORS error. Those actions are not equivalent. Opening a URL displays a resource. A script running on one origin is asking to read a response from another origin.
The browser’s same-origin protections limit that read. Cross-Origin Resource Sharing lets a server declare which origins may read its response by sending HTTP headers. MDN explains that certain methods or headers can also trigger a preflight `OPTIONS` request before the actual request in its CORS guide.
Access-Control-Allow-Origin: https://your-site.exampleThe exact header must match the server’s policy and the request’s credentials rules. Adding mode: "no-cors" is not a general fix: it can produce an opaque response that JavaScript cannot inspect as normal JSON. If the API does not allow your origin, the durable solutions are server-side configuration, a permitted backend proxy, or choosing an API intended for browser use. Do not try to bypass the browser’s security boundary.
CORS errors are intentionally vague to JavaScript. The browser console and Network panel provide the useful detail: whether an `OPTIONS` preflight occurred, which response header was missing, and which origin made the request. This is why the same request may appear to work in a command-line client but fail in the browser.
Move the working request into a complete UI flow
const button = document.querySelector("#load-weather");
const output = document.querySelector("#temperature");
const message = document.querySelector("#weather-message");
async function loadWeather() {
button.disabled = true;
message.textContent = "Loading forecast…";
try {
const data = await getForecast();
const temperature = data.current.temperature_2m;
if (typeof temperature !== "number") {
throw new Error("The API returned no usable temperature");
}
output.textContent = `${temperature}°C`;
message.textContent = "Forecast loaded.";
} catch (error) {
console.error(error);
message.textContent = "We could not load the forecast. Try again.";
} finally {
button.disabled = false;
}
}
button.addEventListener("click", loadWeather);The UI now has a loading state, a success state, a user-safe error message, and a final cleanup step. The user does not need a stack trace, but the developer does need the console error during investigation. This is where the DOM and asynchronous JavaScript meet; our DOM guide and Promises and async/await article cover those foundations in depth.
Authentication belongs on the correct side of the boundary
Many APIs require an API key, bearer token, cookie, or signed request. A public frontend cannot safely hide a long-lived secret in JavaScript shipped to every visitor. If the request requires a private credential, move the sensitive call to a server you control and let the browser talk to that server under an appropriate session or access policy.
// Do not publish a real secret in browser JavaScript.
const headers = {
Authorization: `Bearer ${PUBLICLY_VISIBLE_VALUE}`,
};Store configuration and secrets outside committed source, as explained in our guide to environment variables and `.env` files. An environment variable is not automatically secret when it is bundled into a frontend application; inspect where the value ends up at build time.
One response may not be the whole dataset
Search and list endpoints commonly paginate. GitHub’s REST documentation explains that an endpoint may return a subset of results and provide links in the response headers for previous, next, first, or last pages in its pagination guide. If your UI displays 30 issues, that may be one page of a larger collection, not the complete dataset.
const response = await fetch("https://api.example.com/items?per_page=20");
const items = await response.json();
const nextPage = response.headers.get("Link");Read the provider’s pagination contract. Some APIs use page numbers, others cursor tokens, dates, or a `next` URL. Do not assume that adding `page=2` works for every service.
Rate limits turn loops into incidents
An API provider may limit how many requests a client can make during a period. A 429 response is a signal to slow down, inspect request volume, and follow the provider’s retry guidance. Rendering a component that fetches on every keystroke, refreshing in several effects, or retrying immediately in a loop can create the very outage you are trying to recover from.
Cache stable data when appropriate, debounce search input, stop retrying after a sensible limit, and show the user what is happening. Our CDN guide explains caching at the delivery layer; API caching also requires attention to freshness, authorization, and whether the response is safe to share.
The debugging sequence to keep
- Reproduce the request and copy the exact URL, method, and parameters.
- Check whether the browser made a network request or blocked it before sending.
- Read the status code before parsing the body.
- Inspect the raw response and compare its shape with the API documentation.
- Check CORS headers and preflight behavior for cross-origin browser requests.
- Confirm where authentication belongs and remove secrets from client code.
- Check pagination, rate limits, retries, and duplicate requests.
- Only then change the UI code that displays the result.
This sequence is intentionally slower than copying a one-line example. It is faster than changing five layers at once. When an API call fails, make one layer observable, test it, and move to the next.
Questions beginners usually ask
Why does `fetch()` not throw for a 404?
A 404 is still an HTTP response, so the Promise can resolve. Check `response.ok` or the status explicitly before trusting the body. Network failures are a different class of error and may reject the Promise.
Can I fix CORS by adding `no-cors`?
Usually not. `no-cors` restricts what the browser and script can inspect. Configure the server, use an approved backend proxy, or choose an API that permits the browser origin.
Should an API key be placed in frontend JavaScript?
Assume that users can inspect shipped frontend code. Private, long-lived credentials belong on a server-side boundary, not in a public bundle or README.
Why is the API response missing items?
Check pagination, filters, permissions, and rate limits. Many APIs intentionally return a page of results and provide a link or cursor for the next page.
Make the contract visible
A successful API integration is not just a `fetch()` call that printed a value once. It is a client that knows what it requested, distinguishes network failures from HTTP failures, checks the response shape, respects browser security, protects credentials, and handles the provider’s pagination and rate rules.
Once you can debug those layers, APIs stop feeling like mysterious remote functions. They become what they are: documented contracts between systems, with evidence available at every step when you know where to look.
Continue with the asynchronous behavior behind these requests: Promises and async/await →

Alex Carter is the editorial name behind Vandutz Academy, a programming blog for beginners. Alex reviews and tests the examples and explanations published on the site, with a focus on making Python, JavaScript, web development, and developer tools easier to understand.