---
title: "Errors and retries"
description: "One exception type for every refusal, a property for every kind, and retries that cannot send twice."
url: "https://openemail.uk/docs/csharp/errors"
area: "C#"
category: "Getting started"
---

# Errors and retries

One exception type for every refusal, a property for every kind, and retries that cannot send twice.

## Handling an error

**errors.cs**

```
try
{
    await client.Emails.SendAsync(new Body
    {
        ["from"] = "billing@acme.com",
        ["to"] = "ada@example.com",
        ["subject"] = "Your September invoice",
        ["text"] = "Invoice attached.",
    });
}
catch (OpenEmailApiException error) when (error.IsValidation)
{
    Console.Error.WriteLine($"{error.Code} {error.Param} {error.Message}");
}
catch (OpenEmailApiException error) when (error.IsRateLimited)
{
    Console.Error.WriteLine($"try again in {error.RetryAfterSeconds} seconds");
}
catch (OpenEmailNetworkException error) when (error.IsTimeout)
{
    Console.Error.WriteLine("no answer in time");
}
```

A refusal from the API throws `OpenEmailApiException`, and no response at all throws `OpenEmailNetworkException`. Both derive from `OpenEmailException`, so one `catch` takes either, and a `when` filter picks the kind.

| Property | What it holds |
| --- | --- |
| `Status` | The HTTP status. |
| `ErrorType`, `Code` | The error type and the code of the API, such as `validation_error` and `invalid_parameter`. |
| `Param` | The field that is to blame, when there is one. |
| `RequestId` | The id to quote when you write to support. |
| `DocUrl` | The page of the docs that explains the code. |
| `RetryAfterSeconds` | How long the API asked you to wait, in seconds. |
| `Fields` | The fields a sign-up form refused, each with a `key` and an `error`. |
| `Body` | The decoded response body. |

## The kinds of failure

| Property | When it is true |
| --- | --- |
| `IsAuth` | The key or token was refused, with a 401. |
| `IsPermission` | The credential may not do this, with a 403. |
| `IsScopeMissing` | The credential lacks the scope the call needs. |
| `IsStepUpRequired` | An access token has to verify a code first. |
| `IsNotFound` | Nothing has that id, with a 404. |
| `IsConflict` | The change conflicts with the current state, with a 409. |
| `IsValidation` | A field was refused, with a 422. |
| `IsRateLimited` | Too many requests, or an allowance is spent, with a 429. |
| `IsInvalidRequest` | Any other refusal of the request. |
| `IsServerError` | The API failed, with a status of 500 or more. |
| `IsRetryable` | The status is one the client retries. |
| `IsTimeout` | No response arrived before the timeout. This one is on `OpenEmailNetworkException`. |

A mistake in the call itself, such as a malformed key or an empty id, throws `ArgumentException` before anything is sent. A cancelled token throws `OperationCanceledException`, and a webhook delivery that fails its check throws `OpenEmailWebhookException`.

## A verification code

An access token acts for a person, so before a sensitive change, such as deleting a domain, it is asked for the same verification code the web app asks for. Ask for a code, check it, then make the request again. API keys are never asked for one.

**step-up.cs**

```
var domainId = "b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f";

try
{
    await client.Domains.DeleteAsync(domainId);
}
catch (OpenEmailApiException error) when (error.IsStepUpRequired)
{
    var challenge = await client.Security.BeginStepUpAsync();

    Console.WriteLine($"Enter the code sent by {challenge["method"]}");

    await client.Security.VerifyStepUpAsync(new Body { ["code"] = Console.ReadLine()?.Trim() });
    await client.Domains.DeleteAsync(domainId);
}
```

## What is tried again

- Reads, sends and every write that is safe to repeat are tried again, up to `MaxRetries` times. Any other write is sent once.
- The statuses 408, 500, 502, 503 and 504 are retried with a backoff that starts at half a second and doubles up to eight seconds.
- A 429 is retried only when it carries a `Retry-After` of a minute or less, and the client waits that long.
- A connection that fails or times out is retried the same way.

A send that is retried carries the same idempotency key every time, so the API replays the first message instead of sending a second one.
