Contents Lesson 8 of 16

7 min read · foundations

When the data does not arrive

Everything so far assumed the response comes back and is correct. Most of the time it does. The rest of the time is what decides whether you trust your own tool at 7am.

Four ways a market tool fails, and they are not the same

Generated code usually has one failure branch, if any. There are four, and they need different answers.

No key. The environment variable is missing: a fresh clone, or a deploy where you forgot to set it. The tool cannot work at all, and the fix is a specific action you can name. Say exactly that: copy .env.example, add the key, restart. Never a blank page.

A rejected key. The key exists but the API said no: revoked, or a plan that does not cover the instrument you asked for. Same visible symptom, completely different fix. Distinguishing them saves an hour of looking in the wrong place.

Out of quota. You have spent the day's allowance. Your tool is fine and will work again tomorrow, but the current screen has no data, and the honest thing is to say why, with the meter beside it. This is not an error in your code, and dressing it up as one teaches you to distrust working software.

A bad ticker. One symbol in the list does not exist or is not covered. The list is not broken; one row is. Fail that row, keep the others.

That last one is the design principle for the whole screen: partial data is not failure. Eleven rows and one hole is a working tool. Eleven rows thrown away because of one hole is a tool that will annoy you into rewriting it.

Check the response before you parse it

The single most common omission in generated fetch code:

const res = await fetch(url);
if (!res.ok) {
  return { error: `EODHD ${res.status}` };   // 402, 429, 404 all land here
}
const body = await res.json();

Without the res.ok check, res.json() happily parses an error body into your table and you render blanks. The status code is the difference between "your plan does not cover this" and "you are out of calls", and it is free information you threw away.

Timeouts, because a hung request is worse than a failed one

A request that never returns leaves your page spinning forever. Give every outbound call a deadline:

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 15_000);
try {
  return await fetch(url, { signal: controller.signal });
} finally {
  clearTimeout(timer);
}

The finally is not decoration: without it a fast response leaves a timer running, and enough of those is a leak.

"NA" is a value, not an error

Market APIs return placeholder strings where a number is unavailable: a field that has never been reported, a market that has not opened. Your code asks for a number and gets "NA", which is not null, not an error status, and will happily flow into your formatting function and render as NaN.

Validate the shape at the boundary, once, rather than defending in every component:

function isQuote(row: any): boolean {
  return typeof row.code === "string"
    && typeof row.close === "number"
    && typeof row.change === "number";
}

Rows that fail get dropped or marked; rows that pass are trustworthy everywhere downstream. One check at the edge beats twenty defensive checks in the UI.

Empty states teach; blank pages do not

Three screens deserve real writing, and all three are places a person is confused:

  • Empty list names the file to edit: "Your watchlist is empty. Add tickers in src/lib/watchlist.ts."
  • Loading is a skeleton in the shape of the table, so nothing jumps when data lands.
  • Failed gives the specific cause and the specific next step, from the four above.

Write these before you need them. You will need them on the morning you are in a hurry, which is precisely when a blank page costs the most.

Try it now

Produce the failures on purpose and look at what your tool shows. Remove the key from .env.local and restart. Put a nonsense ticker like NOTREAL.US in the list. Corrupt the key by one character. Quota you cannot exhaust on demand, so that branch waits for a real morning. For each one, ask: does the screen tell me which failure this is, and what to do next? Fix the ones that do not.

This is what the API sends back to the tool's multi-symbol quote request in each case, measured on 28 September 2026:

What you did Status Content-Type Body
Removed the key (no api_token at all) 401 text/html Unauthenticated
Left api_token=undefined, what a template string makes of a missing variable 401 text/html Unauthenticated
Corrupted the key by one character 401 text/html Unauthenticated
Added NOTREAL.US to the list 200 application/json the other rows as normal, plus a NOTREAL.US row whose prices, volume and change fields are all "NA"

Two things follow. The API cannot tell a missing key from a wrong one, so your tool has to: check that the variable exists before it sends anything. And the bad ticker is not an error status at all; it arrives as a row of "NA", which is exactly what the shape check at the boundary is for.