# Inngest errors

> Control whether a failed step retries and when its next attempt starts.

Stop retrying a permanent error or delay the next attempt until a provider is ready. A standard error follows the default retry policy; `NonRetriableError` and `RetryAfterError` change that behavior.

## Stop retries for a permanent error

Throw `NonRetriableError` when the same input cannot succeed on another attempt, such as an ID that fails validation. It bypasses remaining retries for the function or step where you throw it. A failed step can still be caught by the function. See [Non-retriable errors](/docs-markdown/durable-execution/guides-and-advanced/error-handling/non-retriable-errors).

## Retry when an upstream service is ready

Throw `RetryAfterError` when a rate limit or temporary condition provides a useful delay. The second argument accepts milliseconds, a duration string such as `"30s"`, or a `Date`. If you throw a standard error instead, Inngest uses its normal backoff.
This example uses the `inngest` client from the [Quick start](/docs-markdown/durable-execution/quick-start).

```typescript {{ title: "TypeScript" }}
import { NonRetriableError, RetryAfterError } from "inngest";

export const fetchItem = inngest.createFunction(
  {
    id: "fetch-item",
    triggers: { event: "store/item.requested" },
  },
  async ({ event, step }) => {
    if (!event.data.itemId) {
      throw new NonRetriableError("itemId is required");
    }

    return await step.run("fetch-item", async () => {
      const response = await fetch(
        `https://api.example.com/items/${event.data.itemId}`
      );

      if (response.status === 404) {
        throw new NonRetriableError("item does not exist");
      }
      if (response.status === 429) {
        throw new RetryAfterError("item API rate limit", "30s");
      }
      if (!response.ok) {
        throw new Error(`item API returned ${response.status}`);
      }

      return response.json();
    });
  }
);
```

```python {{ title: "Python" }}
import datetime

import httpx
import inngest

@inngest_client.create_function(
    fn_id="fetch-item",
    trigger=inngest.TriggerEvent(event="store/item.requested"),
)
async def fetch_item(ctx: inngest.Context) -> inngest.JSON:
    item_id = ctx.event.data.get("itemId")
    if not item_id:
        raise inngest.NonRetriableError("itemId is required")

    async def fetch() -> inngest.JSON:
        async with httpx.AsyncClient() as http:
            response = await http.get(
                f"https://api.example.com/items/{item_id}"
            )

        if response.status_code == 404:
            raise inngest.NonRetriableError("item does not exist")
        if response.status_code == 429:
            raise inngest.RetryAfterError(
                "item API rate limit", datetime.timedelta(seconds=30)
            )
        if not response.is_success:
            raise Exception(f"item API returned {response.status_code}")

        return response.json()

    return await ctx.step.run("fetch-item", fetch)
```

```go {{ title: "Go" }}
import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/inngest/inngestgo"
	"github.com/inngest/inngestgo/step"
)

type ItemRequestedData struct {
	ItemID string `json:"itemId"`
}

func FetchItem(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "fetch-item"},
		inngestgo.EventTrigger("store/item.requested", nil),
		func(ctx context.Context, input inngestgo.Input[ItemRequestedData]) (any, error) {
			itemID := input.Event.Data.ItemID
			if itemID == "" {
				return nil, inngestgo.NoRetryError(errors.New("itemId is required"))
			}

			return step.Run(ctx, "fetch-item", func(ctx context.Context) (map[string]any, error) {
				resp, err := http.Get(fmt.Sprintf("https://api.example.com/items/%s", itemID))
				if err != nil {
					return nil, err
				}
				defer resp.Body.Close()

				switch {
				case resp.StatusCode == http.StatusNotFound:
					return nil, inngestgo.NoRetryError(errors.New("item does not exist"))
				case resp.StatusCode == http.StatusTooManyRequests:
					// Go takes an absolute retry time rather than a duration.
					return nil, inngestgo.RetryAtError(
						errors.New("item API rate limit"),
						time.Now().Add(30*time.Second),
					)
				case resp.StatusCode >= 300:
					return nil, fmt.Errorf("item API returned %d", resp.StatusCode)
				}

				var item map[string]any
				err = json.NewDecoder(resp.Body).Decode(&item)
				return item, err
			})
		},
	)
}
```

Replace the URL with your service endpoint. If that service supplies a valid `Retry-After` value, pass an appropriate duration or time instead of the fixed `"30s"` shown here.

## Handle an exhausted step

After a step exhausts retries, it throws a `StepError` into the function. Use `try`/`catch` around that step to run a fallback or compensation step. If you do not catch it, the function fails. See [Rollbacks](/docs-markdown/durable-execution/guides-and-advanced/error-handling/rollbacks). Avoid treating every error as permanent; ordinary errors are appropriate when another attempt can succeed.
The SDK also accepts an original `Error` as the optional `cause` of `NonRetriableError` or `RetryAfterError`. This preserves context for diagnosis.