# Handle errors

Learn how to handle common errors during extension runs.

This guide covers how to handle errors when your script calls external endpoints using `endpointFetch`. Before you begin, make sure you’ve [created an extension](https://docs.stripe.com/extensions/custom-actions/build-with-script.md) and set up an [endpoint to invoke](https://docs.stripe.com/extensions/invoke-endpoints.md).

> Currently, Stripe only supports invoking endpoints from custom workflow actions. See [Build a custom action with a script](https://docs.stripe.com/extensions/custom-actions/build-with-script.md).

You don’t need to import `endpointFetch` because it’s a global function injected by the Stripe runtime. It’s available at runtime only, not in tests or local builds. It throws exceptions on non-2xx HTTP responses, network failures, or misconfigurations.

## Handle errors in a script

You can handle recoverable HTTP errors directly in your script. In this example, the script catches a 404 response and falls back to creating the missing resource, while allowing non-recoverable errors such as `ERR_CONFIG` and `ERR_NETWORK` to propagate.

```ts
try {
  const result = await endpointFetch({
    endpoint: 'com.my_script.send_notifications',
    path: '/api/notifications',
    method: 'POST',
    body: JSON.stringify({
      message: `Payment received from ${customInput.name}`,
    }),
  });

  const data = JSON.parse(result.body);
  return { success: true, ts: data.ts };
} catch (e) {
  if (e.status === 404) {
    const result = await endpointFetch({
      endpoint: 'com.my_script.send_notifications',
      path: '/api/contacts',
      method: 'POST',
      body: JSON.stringify({ id: customInput.id, email: customInput.email }),
    });
    return { contact: JSON.parse(result.body), created: true };
  }

  throw e;
}
```

## Success responses (2xx) 

The endpoint returns a `2xx` status code.

In your script, you get a `result` object:

```ts
const result = await endpointFetch({
  endpoint: 'slack_api',
  path: '/chat.postMessage',
  method: 'POST',
  body: JSON.stringify({ channel: '#billing-alerts', text: 'Invoice paid' }),
});

// result = { ok: true, status: 200, body: '{"ok":true,"channel":"C123","ts":"1234567890.123456"}' }
```

Typical handling:

```ts
const result = await endpointFetch({
  endpoint: 'slack_api',
  path: '/chat.postMessage',
  method: 'POST',
  body: JSON.stringify({ channel: '#billing-alerts', text: 'Invoice paid' }),
});

const data = JSON.parse(result.body);
return { posted: true, ts: data.ts };
```

## Client errors (4xx) 

The endpoint rejects the request with a `4xx` status code, such as `400 Bad Request`, `403 Forbidden`, `404 Not Found`, or `429 Rate Limited`.

In your script, `endpointFetch` throws:

```ts
await endpointFetch({
  endpoint: 'slack_api',
  path: '/chat.postMessage',
  method: 'POST',
  body: JSON.stringify({ channel: '#nonexistent', text: 'hello' }),
});

// Throws: Error {
//   message: "Endpoint returned HTTP 400",
//   code: "EXT_BAD_REQUEST",
//   status: 400,
//   body: '{"ok":false,"error":"channel_not_found"}',
//   stack: "..."
// }
```

The error includes:

- `e.message`: `"Endpoint returned HTTP {status}"`
- `e.code`: An `EXT_*` code mapped from the HTTP status
- `e.status`: The numeric HTTP status code
- `e.body`: The response body string, or `null` if the endpoint returns an empty response
- `e.stack`: The standard V8 stack trace

Typical handling:

```ts
try {
  const result = await endpointFetch({
    endpoint: 'slack_api',
    path: '/chat.postMessage',
    method: 'POST',
    body: JSON.stringify({ channel, text: message }),
  });
  const data = JSON.parse(result.body);
  return { success: true, ts: data.ts };
} catch (e) {
  if (e.status === 429) {
    return { success: false, retriable: true, reason: 'rate_limited' };
  }
  if (e.status) {
    const error = e.body ? JSON.parse(e.body) : {};
    return { success: false, status: e.status, error: error.error };
  }
  throw e; // re-throw non-HTTP errors
}
```

## Server errors (5xx) 

The endpoint reports a server-side failure with a `5xx` status code, such as `HTTP 500`, `502 Bad Gateway`, or `503 Service Unavailable`.

In your script, the error matches `4xx` errors: an exception with `code`, `status`, `body`, and `stack`. `body` might be `null` if the endpoint returns an empty response.

```ts
await endpointFetch({
  endpoint: 'slack_api',
  path: '/chat.postMessage',
  method: 'POST',
  body: JSON.stringify({ channel: '#billing-alerts', text: 'Invoice paid' }),
});

// Throws: Error {
//   message: "Endpoint returned HTTP 503",
//   code: "EXT_RESOURCE_UNAVAILABLE",
//   status: 503,
//   body: null,
//   stack: "..."
// }
```

Typical handling:

```ts
try {
  const result = await endpointFetch({
    endpoint: 'slack_api',
    path: '/chat.postMessage',
    method: 'POST',
    body: JSON.stringify({ channel, text: message }),
  });
  return { success: true };
} catch (e) {
  if (e.status >= 500) {
    return { success: false, retriable: true, status: e.status };
  }
  throw e;
}
```

## Network errors 

Network errors occur when Stripe accepts the request, but the call to the endpoint fails. That includes:

- DNS resolution failure for the endpoint host
- Connection refused by the endpoint
- Endpoint request timeout

If you don’t catch the error, the script run fails with a `runtime_error` error code (non-retryable).

In your script:

```ts
await endpointFetch({
  endpoint: 'slack_api',
  path: '/chat.postMessage',
  method: 'POST',
  body: JSON.stringify({ channel: '#billing-alerts', text: 'Invoice paid' }),
});

// Throws: Error { message: "Network error", code: "ERR_NETWORK", stack: "..." }
```

Properties:

- `e.message`: Always `"Network error"`
- `e.code`: `"ERR_NETWORK"`
- `e.stack`: The standard V8 stack trace

Hostnames and low-level connection details aren’t exposed to scripts for security reasons.

## Configuration errors

Configuration errors occur if Stripe can’t find a resource the script references. That includes:

- The endpoint name in the app manifest doesn’t exist
- The script ID isn’t found

Fix the manifest or deployment. If uncaught, the script run fails with a `bad_request` error code (non-retryable).

In your script:

```ts
await endpointFetch({
  endpoint: 'nonexistent_api',
  path: '/test',
  method: 'GET',
});

// Throws: Error { message: "Endpoint not found: nonexistent_api", code: "ERR_CONFIG" }
```

The error has the following properties:

- `e.message`: Describes what’s missing (for example,`"Script not found: scp_123"`, `"Endpoint not found: slack_api"`, or `"Resource not found"` when details aren’t available)
- `e.code`: `"ERR_CONFIG"`

There are no `e.status` or `e.body` properties. This isn’t an HTTP response.

In this case, let the error propagate. It’s a configuration or deployment issue, not something to handle at runtime in the script.

## Error scenarios summary

Stripe doesn’t automatically retry failed `endpointFetch` calls. To retry, handle it in your script.

| Scenario            | Exception? | `e.code`                   | `e.status` | `e.body`             | Retryable? |
| ------------------- | ---------- | -------------------------- | ---------- | -------------------- | ---------- |
| `2xx success`       | No         | N/A                        | N/A        | N/A                  | N/A        |
| `400 Bad Request`   | Yes        | `EXT_BAD_REQUEST`          | 400        | Error body or `null` | No         |
| 401 Unauthorized    | Yes        | `EXT_UNAUTHORIZED`         | 401        | Error body or `null` | No         |
| `403 Forbidden`     | Yes        | `EXT_NOT_ALLOWED`          | 403        | Error body or `null` | No         |
| `404 Not Found`     | Yes        | `EXT_NOT_FOUND`            | 404        | Error body or `null` | No         |
| `429 Rate Limited`  | Yes        | `EXT_RATE_LIMIT`           | 429        | Error body or `null` | Yes        |
| Other 4xx           | Yes        | `EXT_BAD_REQUEST`          | 4xx        | Error body or `null` | No         |
| HTTP 500            | Yes        | `EXT_RUNTIME_ERROR`        | 500        | Error body or `null` | No         |
| `503 Unavailable`   | Yes        | `EXT_RESOURCE_UNAVAILABLE` | 503        | Error body or `null` | Yes        |
| `504 Timeout`       | Yes        | `EXT_TIMEOUT`              | 504        | Error body or `null` | Yes        |
| Other 5xx           | Yes        | `EXT_RUNTIME_ERROR`        | 5xx        | Error body or `null` | No         |
| Network failure     | Yes        | `ERR_NETWORK`              | N/A        | N/A                  | No         |
| Configuration error | Yes        | `ERR_CONFIG`               | N/A        | N/A                  | No         |

## Next steps

- [Build a custom action with a script](https://docs.stripe.com/extensions/custom-actions/build-with-script.md)
- [Invoke an endpoint from a script](https://docs.stripe.com/extensions/invoke-endpoints.md)
- [Extension points](https://docs.stripe.com/extensions/extension-points.md)
- [How extensions work](https://docs.stripe.com/extensions/how-extensions-work.md)
