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:
- A function throws an error. ✅ This will be retried (see retry policies)
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)