Handling Errors & Retries

When functions fail, Inngest can retry them automatically. Whether Inngest retries your function is determined by how it fails. To learn how to handle function failures, read the reference guide here.

Types of failures

There are two ways that functions are determined to have failed:

export default inngest.createFunction(
  { id: "import-item-data" },
  { event: "import.requested" },
  async ({ event }) => {
    throw new Error("Failed to fetch item from ecommerce API");
  }
);
  • A function throws a non-retriable error. ❌ This will not be retried.
import { NonRetriableError } from "inngest";

export default inngest.createFunction(
  { id: "mark-store-imported" },
  { event: "import.completed" },
  async ({ event }) => {
    try {
      const result = await database.updateStore(
        { id: event.data.storeId },
        { imported: true }
      );
      return result.ok === true;
    } catch (err) {
      // Passing the original error via `cause` enables you to view the error in function logs
      throw new NonRetriableError("Store not found", { cause: err });
    }
  }
);

Attempt counter

Every time your function is executed, an attempt count is passed in as input, which is the current zero-based attempt number for this run.

The first attempt will be 0, the second 1, and so on. It is incremented every time the function throws an error and is retried.

inngest.createFunction(
  { id: "product-check" },
  { event: "product.check.requested" },
  async ({ attempt }) => {
    // `attempt` is the zero-based attempt number
  }
);

If retries are caused by a step failing which then succeeds before retries are exhausted, attempt will reset back to 0. See the Errors within steps section below.

Errors within steps

Steps are individually retried. Inngest will handle the type of error, just as above, but on the per-step basis.

When a step exhausts all retries, it will throw a StepError, which matches the shape of an Error.

async ({ step, logger }) => {
  // This is unhandled, so will fail the function
  await step.run("Unhandled error", () => {
    throw new Error("Oh no!");
  });

  // We can use a try/catch block to handle an error and recover
  try {
    await step.run("try/catch error", () => {
      throw new Error("Oh no!");
    });
  } catch (err) {
    await step.run("Recover with try/catch", () => {
      // ...
    });
  }

  // Or do the same with promise chaining
  await step
    .run("Chain error", () => {
      throw new Error("Oh no!");
    })
    .catch((err) => {
      return step.run("Recover with chaining", () => {
        // ...
      });
    });

  // Non-critical steps can be performed by swallowing the error
  await step
    .run("Ignore the error", () => {
      throw new Error("Oh no!");
    })
    .catch((err) => logger.error(err));

  // A non-retriable error can still be thrown from within a step to cancel the function
  await step.run("Cancel the function", () => {
    throw new NonRetriableError("Stopping");
  });
};

Retry policies

By default, each function is retried 3 times using backoff with jitter.

  • Successful - No error thrown. This will not be retried.
  • Non-retriable error - A NonRetriableError was thrown (think: 404). This will not be retried.
  • Error - Any error was thrown indicating a potentially temporary failure (think: 500). This will be retried according to the retry policy (3 times, by default).

You can customize the number of retries directly from your function configuration:

import { Inngest } from "inngest";

const inngest = new Inngest({ id: "my-app" });

export default inngest.createFunction(
  {
    id: "handle-form",
    retries: 10, // Choose the number of retries you'd liie.
  },
  { event: "api/form.submitted" },
  async ({ event, step }) => {
    // ...
  }
);

Retries follow this backoff schedule, with 0-30 seconds of random jitter added.

You can manually schedule a retry by throwing RetryAfterError:

// Retry in 5 seconds
throw new RetryAfterError("your error message", 5 * 1000)