Examples · Web Development
Errors, retries and timeouts in JavaScript
Make the API fail on purpose, with a wrong key, a model that does not exist, a parameter it does not know, a timeout and a user who leaves, and read what really comes back. The library tries again only when that can help; your code turns each error into a message the user can act on.
Intermediate · 30 min
The idea
On your computer, every call works. In front of real users, some fail. Each error belongs to one of three groups, and each group needs a different reaction:
| Group | Examples | Try again? | What to do |
|---|---|---|---|
| A mistake in your code | 400, 401, 404 | No: the same request gets the same error | Read the message, fix the code |
| A busy moment | 429, 500–599, network, timeout | Yes, a little later | The library tries again for you |
| The user left | closed page, Stop button | No | Stop, and tell nobody |
What you need
Node.js 22, the openai package and a key in API_KEY, as in Your first API call in JavaScript.
The program: five errors on purpose
Save it as errors.mjs. Each entry in tries breaks one thing; the loop prints what comes back, and what the user would read.
import OpenAI from "openai";
const settings = { baseURL: "https://api.ai.itsincom.org/v1", apiKey: process.env.API_KEY };
const client = new OpenAI(settings);
const hello = [{ role: "user", content: "Say hello in Italian." }];
const long = [{ role: "user", content: "Explain in about 400 words how a web browser loads a page." }];
// What the person in front of your page reads: short, and it says what to do.
function friendly(err) {
if (err instanceof OpenAI.APIUserAbortError) return null; // they left: nobody to tell
if (err instanceof OpenAI.APIConnectionTimeoutError) return "The answer is taking too long. Please try again.";
if (err instanceof OpenAI.APIConnectionError) return "The assistant cannot be reached. Check your connection.";
if (err instanceof OpenAI.RateLimitError) return "Too many questions right now. Please wait a minute.";
if (err instanceof OpenAI.InternalServerError) return "The assistant has a problem. Please try again later.";
return "Sorry, something is wrong on our side."; // 400, 401, 404: a bug to fix, not to retry
}
const leave = new AbortController();
const tries = {
"a key with its last character missing": () =>
new OpenAI({ ...settings, apiKey: process.env.API_KEY.slice(0, -1) })
.chat.completions.create({ model: "qwen3.8-27b", messages: hello }),
"a model that does not exist": () =>
client.chat.completions.create({ model: "qwen3.8", messages: hello }),
"a parameter the API does not know": () =>
client.chat.completions.create({ model: "qwen3.8-27b", messages: hello, enable_thinking: false }),
"a timeout of 2 seconds, no retries": () =>
client.chat.completions.create({ model: "qwen3.8-27b", messages: long }, { timeout: 2000, maxRetries: 0 }),
"a user who leaves after 2 seconds": () => {
setTimeout(() => leave.abort(), 2000);
return client.chat.completions.create({ model: "qwen3.8-27b", messages: long }, { signal: leave.signal });
},
};
for (const [what, call] of Object.entries(tries)) {
const started = Date.now();
try {
await call();
console.log(`${what}: no error`);
} catch (err) {
console.log(`${what}, after ${Date.now() - started} ms:`);
console.log(` ${err.constructor.name}, status ${err.status}, type ${err.type}, request ${err.requestID}`);
console.log(` ${err.message}`);
console.log(` the user reads: ${friendly(err) ?? "(nothing: they left)"}`);
}
}
Run it with node errors.mjs. It takes about five seconds.
What you should see
a key with its last character missing, after 98 ms:
AuthenticationError, status 401, type invalid_request_error, request req_a4f86e50fc6432c8953bd427
401 invalid api key
the user reads: Sorry, something is wrong on our side.
a model that does not exist, after 72 ms:
NotFoundError, status 404, type model_not_found, request req_19ca83a36932330ab5e77634
404 The model 'qwen3.8' does not exist. GET /v1/models lists the models you can use, and the catalogue describes them: https://wiki.ai.itsincom.org/live/models
the user reads: Sorry, something is wrong on our side.
a parameter the API does not know, after 62 ms:
BadRequestError, status 400, type invalid_request_error, request req_28ae612bcc06f03ee8c79a0e
400 Unrecognized request argument supplied: 'enable_thinking'. We refuse what we do not know instead of ignoring it: an argument accepted and dropped does not show in the answer. Accepted: frequency_penalty, logit_bias, logprobs, max_completion_tokens, max_tokens, messages, model, n, parallel_tool_calls, presence_penalty, response_format, seed, service_tier, stop, stream, stream_options, temperature, tool_choice, tools, top_logprobs, top_p, user.
the user reads: Sorry, something is wrong on our side.
a timeout of 2 seconds, no retries, after 2007 ms:
APIConnectionTimeoutError, status undefined, type undefined, request undefined
Request timed out.
the user reads: The answer is taking too long. Please try again.
a user who leaves after 2 seconds, after 2002 ms:
APIUserAbortError, status undefined, type undefined, request undefined
Request was aborted.
the user reads: (nothing: they left)
Read the error, not only the number
The school's API says what is wrong, in plain words. Print err.message in your terminal or your log, and read it.
- 401,
invalid api key. One character missing, as when you copy the key badly. The answer comes back in 0.1 seconds. - 404,
model_not_found. The model is calledqwen3.8-27b, notqwen3.8. The message tells you where the list is. - 400,
Unrecognized request argument supplied: 'enable_thinking'. Guides for Qwen models showenable_thinking: falseto switch the thinking off, so it is a natural thing to try. The school's API refuses every parameter it does not know, and says why: an argument accepted and dropped would not show in the answer, and you would believe it worked. The message also lists every parameter it accepts. err.requestIDcomes from thex-request-idheader. Write it in your log: when you ask for help, it lets the staff find your request.
None of these three becomes right by trying again, and the library knows it: it does not try again on a 400, a 401 or a 404. Your code shows a short apology and keeps the details for you.
APIConnectionTimeoutError and APIUserAbortError have no status: no HTTP answer arrived. Order matters in friendly(): a timeout is also an APIConnectionError, so the timeout is checked first.
What the library does for you
By default the openai library tries a request 3 times in all (maxRetries: 2) when trying again can help: timeouts and network errors, 408, 409, 429, and every 500 or above. Save this as retries.mjs:
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.ai.itsincom.org/v1",
apiKey: process.env.API_KEY,
timeout: 2000, // 2 seconds for each try: far too short, on purpose
logLevel: "info", // the library says when it tries again
});
const started = Date.now();
try {
await client.chat.completions.create({
model: "qwen3.8-27b",
messages: [{ role: "user", content: "Explain in about 400 words how a web browser loads a page." }],
});
console.log("answered");
} catch (err) {
console.log(`${err.constructor.name} after ${Date.now() - started} ms`);
}
[log_5c518b] connection timed out - retrying, 2 attempts remaining
[log_52dc77] connection timed out - retrying, 1 attempts remaining
[log_05bad5] connection timed out - error; no more retries left
APIConnectionTimeoutError after 7311 ms
Three tries of 2 seconds, and short pauses between them: 7.3 seconds before your code hears anything (7.4 in our other run). Each try is a new request: it counts in your 60 requests a minute. When we sent the enable_thinking request with logLevel: "info", the library logged … failed with status 400 in 102ms - error; not retryable: one try, no pause.
The rule for a web page: with timeout T and maxRetries R, a user can wait (R + 1) × T, plus the pauses, before seeing an error. The defaults are 10 minutes and 2: in the worst case, more than half an hour.
The 429, from the documentation
We did not trigger a 429 on purpose: the limit is per key, 60 requests a minute, and a script that hammers the API to reach it fills that minute for everything else that uses the key. This is what the library's README and code say:
- A 429 becomes a
RateLimitError, and it is one of the errors the library tries again, up tomaxRetriestimes. - Between tries it waits as long as the answer asks, in a
Retry-Afterheader (orretry-after-ms), up to 60 seconds. Without one, it waits about half a second, then one second, doubling up to 8, a little shorter at random so that many clients do not all come back at the same moment. - If the last try is a 429 too, your code gets the
RateLimitError, andfriendly()asks the user to wait a minute.
Do not write your own loop that tries again at once. A loop like that keeps the limit full: every try is one more request in the same minute. If you need more than the library does, wait first, then try once.
In a web app, every visitor uses your key. One user with a script that asks twenty questions at once can fill the minute for all the others: give each user a limit of their own on your server, as in the exercise of A chat box on your web page.
How long is too long?
A timeout that is too long leaves a user waiting for nothing. One that is too short throws away good answers, and pays for each try. Measure your model on your task before you choose.
- “Explain in about 400 words how a web browser loads a page”: with
qwen3.8-27b, 12 seconds without streaming, in two runs. - “Write a story of 300 words about a bike”:
qwen3.8-27bthought before writing, without showing it. In two streamed runs the first word came after 97 and 113 seconds. In a third run, without streaming, the answer took 135 seconds and 17,206 completion tokens, for a story of exactly 300 words.gemma-4-26bwrote 285 words in 3.3 seconds, with 353 tokens. Withtimeout: 30_000, every Qwen try in our runs would have failed: three requests, more than 90 seconds of waiting, no story.
With stream: true, the timeout is the wait until the answer starts, not for the whole answer: the library stops the clock when the answer starts to arrive. On the school's API, it starts with the first words, after the silent thinking. We set timeout: 5000 on the 400-word explanation: the stream started after 1.4 seconds and ran until 15.5 seconds, with no error. That makes streaming the friendlier choice for long answers. But the wait before the first word can be long too: here the story waited 97 seconds before its first word.
A user who leaves
An AbortController stops a request. In the program, a timer calls leave.abort() after 2 seconds; in a web server, the same call goes in res.on("close", ...), as in A chat box on your web page. The library throws an APIUserAbortError: it is not a problem to show, so friendly() returns null, and it does not belong in your error log.
Good to know
err.messageis for you,friendly()is for the user. “400 Unrecognized request argument supplied” means nothing to a visitor, and it tells a curious one how your server talks to the API.- Set
timeoutandmaxRetriesonce, on the client, for your whole app:new OpenAI({ ..., timeout: 60_000, maxRetries: 1 }). Change them for one call with a second argument, as the program does:{ timeout: 2000, maxRetries: 0 }. - An error can come in the middle of a stream. The
for awaitloop throws, and the user has already seen half an answer. Say so after the text, as the chat box does with “[The answer stopped here: try again.]”. - Check
finish_reasoneven when there is no error:lengthmeans the answer was cut, and the text is not complete.
Exercise
- Add
logLevel: "info"tosettingsinerrors.mjs, so that both clients use it. Which requests does the library try again? - In
retries.mjs, set the timeout to 30 seconds and run it: does the answer arrive? Then try the 300-word story, withqwen3.8-27band thengemma-4-26b, and write down the times. - Write
ask(messages, signal): it calls the API, logserr.status,err.requestIDanderr.messagefor you (but not when the user left), and returns{ ok: false, text: friendly(err) }or{ ok: true, text }. Use it in the server of the chat box. - On a
RateLimitError, readerr.headers.get("retry-after")and tell the user how many seconds to wait.
