Skip to content
Fondazione ITS INCOM

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.

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:

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.

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

Exercise

All examples