Skip to content

Error Handling

The VaultSandbox .NET Client provides comprehensive exception handling with automatic retries for transient failures and specific exception types for different failure scenarios.

All SDK exceptions inherit from the base VaultSandboxException class, allowing you to catch all SDK-specific exceptions with a single catch block.

VaultSandboxException (base class)
├── ApiException
├── NetworkException
├── VaultSandboxTimeoutException
├── InboxNotFoundException
├── EmailNotFoundException
├── InboxAlreadyExistsException
├── InvalidImportDataException
├── DecryptionException
├── SignatureVerificationException
├── ServerKeyMismatchException
├── SseException
└── StrategyException

The SDK automatically retries failed HTTP requests for transient errors. This helps mitigate temporary network issues or server-side problems.

By default, requests are retried for these HTTP status codes:

  • 408 - Request Timeout
  • 429 - Too Many Requests (Rate Limiting)
  • 500 - Internal Server Error
  • 502 - Bad Gateway
  • 503 - Service Unavailable
  • 504 - Gateway Timeout

Configure retry behavior when building the client:

using VaultSandbox.Client;
var client = VaultSandboxClientBuilder.Create()
.WithBaseUrl("https://smtp.vaultsandbox.com")
.WithApiKey(Environment.GetEnvironmentVariable("VAULTSANDBOX_API_KEY"))
.WithMaxRetries(5) // Default: 3
.WithRetryDelay(TimeSpan.FromSeconds(2)) // Default: 1s
.Build();

The SDK uses exponential backoff for retries:

  • 1st retry: RetryDelay
  • 2nd retry: RetryDelay * 2
  • 3rd retry: RetryDelay * 4
  • And so on…
// With RetryDelay: 1s and MaxRetries: 3
// Retry schedule:
// 1st attempt: immediate
// 2nd attempt: after 1s
// 3rd attempt: after 2s
// 4th attempt: after 4s
// Total time: up to 7 seconds + request time

Base class for all SDK exceptions. Use this to catch any SDK-specific exception.

public class VaultSandboxException : Exception
{
public override string Message { get; }
}
using VaultSandbox.Client.Exceptions;
try
{
var inbox = await client.CreateInboxAsync();
// Use inbox...
}
catch (VaultSandboxException ex)
{
Console.WriteLine($"VaultSandbox error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex}");
}

Thrown for API-level errors such as invalid requests or permission denied.

public class ApiException : VaultSandboxException
{
public int StatusCode { get; }
public string? ResponseBody { get; }
}
  • StatusCode: HTTP status code from the API
  • ResponseBody: Optional response body from the server
  • Message: Error message from the server
using VaultSandbox.Client.Exceptions;
try
{
var inbox = await client.CreateInboxAsync();
}
catch (ApiException ex)
{
Console.WriteLine($"API Error ({ex.StatusCode}): {ex.Message}");
switch (ex.StatusCode)
{
case 401:
Console.WriteLine("Invalid API key");
break;
case 403:
Console.WriteLine("Permission denied");
break;
case 429:
Console.WriteLine("Rate limit exceeded");
break;
}
}

Thrown when there is a network-level failure (e.g., cannot connect to server).

public class NetworkException : VaultSandboxException
{
public override string Message { get; }
}
using VaultSandbox.Client.Exceptions;
try
{
var inbox = await client.CreateInboxAsync();
}
catch (NetworkException ex)
{
Console.WriteLine($"Network error: {ex.Message}");
Console.WriteLine("Check your internet connection and server URL");
}

Thrown by methods like WaitForEmailAsync() and WaitForEmailCountAsync() when the timeout is reached before the condition is met.

public class VaultSandboxTimeoutException : VaultSandboxException
{
public TimeSpan Timeout { get; }
}
  • Timeout: The timeout duration that was exceeded
  • Message: Error message describing the timeout
using VaultSandbox.Client.Exceptions;
try
{
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(5),
Subject = "Welcome",
UseRegex = true
});
}
catch (VaultSandboxTimeoutException ex)
{
Console.WriteLine("Timed out waiting for email");
Console.WriteLine("Email may not have been sent or took too long to deliver");
// Check what emails did arrive
var emails = await inbox.GetEmailsAsync();
Console.WriteLine($"Found {emails.Count} emails:");
foreach (var e in emails)
{
Console.WriteLine($" - {e.Subject}");
}
}

Thrown when an inbox does not exist on the server.

public class InboxNotFoundException : VaultSandboxException
{
public string EmailAddress { get; }
}
  • EmailAddress: The email address of the inbox that was not found
  • Message: Error message describing the issue
using VaultSandbox.Client.Exceptions;
try
{
var emails = await inbox.GetEmailsAsync();
}
catch (InboxNotFoundException ex)
{
Console.WriteLine("Inbox no longer exists");
Console.WriteLine("It may have expired or been deleted");
}

Thrown when an email does not exist.

public class EmailNotFoundException : VaultSandboxException
{
public string EmailId { get; }
}
  • EmailId: The identifier of the email that was not found
  • Message: Error message describing the issue
using VaultSandbox.Client.Exceptions;
try
{
var email = await inbox.GetEmailAsync("non-existent-id");
}
catch (EmailNotFoundException ex)
{
Console.WriteLine("Email not found");
Console.WriteLine("It may have been deleted");
}

Thrown when attempting to create an inbox with an email address that already exists, or importing an inbox that’s already in the client.

public class InboxAlreadyExistsException : VaultSandboxException
{
public string EmailAddress { get; }
}
  • EmailAddress: The email address of the inbox that already exists
  • Message: Error message describing the issue
using VaultSandbox.Client.Exceptions;
try
{
var inbox = await client.ImportInboxAsync(exportedData);
}
catch (InboxAlreadyExistsException ex)
{
Console.WriteLine("Inbox already imported in this client");
Console.WriteLine("Use a new client instance or delete the existing inbox");
}

Thrown when imported inbox data fails validation (missing fields, invalid keys, server mismatch, etc.).

public class InvalidImportDataException : VaultSandboxException
{
public override string Message { get; }
}
using VaultSandbox.Client.Exceptions;
try
{
var corruptedData = JsonSerializer.Deserialize<InboxExport>(corruptedJson);
var inbox = await client.ImportInboxAsync(corruptedData);
}
catch (InvalidImportDataException ex)
{
Console.WriteLine($"Invalid import data: {ex.Message}");
Console.WriteLine("The exported data may be corrupted or from a different server");
}

Thrown if the client fails to decrypt an email. This is rare and may indicate data corruption or a bug.

public class DecryptionException : VaultSandboxException
{
public override string Message { get; }
}
using VaultSandbox.Client.Exceptions;
try
{
var emails = await inbox.GetEmailsAsync();
}
catch (DecryptionException ex)
{
Console.WriteLine($"Failed to decrypt email: {ex.Message}");
Console.WriteLine("This is a critical error - please report it");
// Log for investigation
Console.WriteLine($"Inbox: {inbox.EmailAddress}");
Console.WriteLine($"Time: {DateTimeOffset.UtcNow:O}");
}

Decryption exceptions should always be logged and investigated as they may indicate:

  • Data corruption
  • SDK bug
  • MITM attack (rare)
  • Server-side encryption issue

Thrown if the cryptographic signature of a message from the server cannot be verified. This is a critical security error that may indicate a man-in-the-middle (MITM) attack.

public class SignatureVerificationException : VaultSandboxException
{
public override string Message { get; }
}
using VaultSandbox.Client.Exceptions;
try
{
var inbox = await client.CreateInboxAsync();
}
catch (SignatureVerificationException ex)
{
Console.WriteLine("CRITICAL: Signature verification failed!");
Console.WriteLine("This may indicate a MITM attack");
Console.WriteLine($"Message: {ex.Message}");
// Alert security team
await AlertSecurityTeamAsync(new SecurityAlert
{
Error = ex.Message,
Timestamp = DateTimeOffset.UtcNow,
ServerUrl = client.Options.BaseUrl
});
throw; // Do not continue
}

Signature verification exceptions should never be ignored:

  1. Log immediately with full context
  2. Alert security/operations team
  3. Stop processing - do not continue with the operation
  4. Investigate - check for network issues, proxy problems, or actual attacks

Thrown when the server’s public key doesn’t match what was expected, potentially indicating a server change or attack.

public class ServerKeyMismatchException : VaultSandboxException
{
public string ExpectedKey { get; }
public string ActualKey { get; }
}
  • ExpectedKey: The expected server signing key (base64url-encoded)
  • ActualKey: The actual server signing key received in the payload (base64url-encoded)
  • Message: Error message describing the mismatch
using VaultSandbox.Client.Exceptions;
try
{
var inbox = await client.ImportInboxAsync(exportedData);
}
catch (ServerKeyMismatchException ex)
{
Console.WriteLine("Server key mismatch detected");
Console.WriteLine("The inbox may have been exported from a different server");
Console.WriteLine($"Details: {ex.Message}");
}

Thrown for errors related to the Server-Sent Events (SSE) connection.

public class SseException : VaultSandboxException
{
public override string Message { get; }
}
using VaultSandbox.Client.Exceptions;
var client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(Environment.GetEnvironmentVariable("VAULTSANDBOX_URL"))
.WithApiKey(Environment.GetEnvironmentVariable("VAULTSANDBOX_API_KEY"))
.UseSseDelivery()
.Build();
try
{
var inbox = await client.CreateInboxAsync();
await foreach (var email in inbox.WatchAsync())
{
Console.WriteLine($"New email: {email.Subject}");
}
}
catch (SseException ex)
{
Console.WriteLine($"SSE connection error: {ex.Message}");
Console.WriteLine("Falling back to polling strategy");
// Recreate client with polling
var pollingClient = VaultSandboxClientBuilder.Create()
.WithBaseUrl(Environment.GetEnvironmentVariable("VAULTSANDBOX_URL"))
.WithApiKey(Environment.GetEnvironmentVariable("VAULTSANDBOX_API_KEY"))
.UsePollingDelivery()
.Build();
}

Thrown when a delivery strategy is not set or is invalid.

public class StrategyException : VaultSandboxException
{
public override string Message { get; }
}
using VaultSandbox.Client.Exceptions;
try
{
var inbox = await client.CreateInboxAsync();
await foreach (var email in inbox.WatchAsync())
{
Console.WriteLine($"New email: {email.Subject}");
}
}
catch (StrategyException ex)
{
Console.WriteLine($"Strategy error: {ex.Message}");
Console.WriteLine("The delivery strategy may not be properly configured");
}
using VaultSandbox.Client;
using VaultSandbox.Client.Exceptions;
var client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(baseUrl)
.WithApiKey(apiKey)
.Build();
try
{
var inbox = await client.CreateInboxAsync();
Console.WriteLine($"Send email to: {inbox.EmailAddress}");
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(10)
});
Console.WriteLine($"Email received: {email.Subject}");
await client.DeleteInboxAsync(inbox.EmailAddress);
}
catch (VaultSandboxTimeoutException)
{
Console.WriteLine("Timed out waiting for email");
}
catch (ApiException ex)
{
Console.WriteLine($"API Error ({ex.StatusCode}): {ex.Message}");
}
catch (NetworkException ex)
{
Console.WriteLine($"Network error: {ex.Message}");
}
catch (VaultSandboxException ex)
{
Console.WriteLine($"VaultSandbox error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex}");
}

Use pattern matching for cleaner exception handling:

try
{
var inbox = await client.CreateInboxAsync();
var email = await inbox.WaitForEmailAsync();
}
catch (Exception ex)
{
var message = ex switch
{
VaultSandboxTimeoutException => "Timed out waiting for email",
ApiException { StatusCode: 401 } => "Invalid API key",
ApiException { StatusCode: 403 } => "Permission denied",
ApiException { StatusCode: 429 } => "Rate limit exceeded",
ApiException api => $"API error ({api.StatusCode}): {api.Message}",
NetworkException net => $"Network error: {net.Message}",
SignatureVerificationException => "CRITICAL: Signature verification failed!",
DecryptionException => "CRITICAL: Decryption failed!",
VaultSandboxException vsb => $"VaultSandbox error: {vsb.Message}",
_ => $"Unexpected error: {ex.Message}"
};
Console.WriteLine(message);
}
async Task<Email> WaitForEmailWithRetryAsync(
IInbox inbox,
WaitForEmailOptions options,
int maxAttempts = 3,
CancellationToken cancellationToken = default)
{
Exception? lastException = null;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
return await inbox.WaitForEmailAsync(options, cancellationToken);
}
catch (VaultSandboxTimeoutException ex)
{
lastException = ex;
Console.WriteLine($"Attempt {attempt}/{maxAttempts} timed out");
if (attempt < maxAttempts)
{
Console.WriteLine("Retrying...");
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
}
}
catch (Exception)
{
// Non-timeout error, don't retry
throw;
}
}
throw lastException!;
}
// Usage
try
{
var email = await WaitForEmailWithRetryAsync(
inbox,
new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(10),
Subject = "Welcome",
UseRegex = true
},
maxAttempts: 3,
cancellationToken);
Console.WriteLine($"Email received: {email.Subject}");
}
catch (VaultSandboxTimeoutException)
{
Console.WriteLine("Failed after retries");
}
async Task<IReadOnlyList<Email>> GetEmailsWithFallbackAsync(
IInbox inbox,
CancellationToken cancellationToken = default)
{
try
{
// Try to wait for new email
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(5)
}, cancellationToken);
return [email];
}
catch (VaultSandboxTimeoutException)
{
Console.WriteLine("No new emails, checking existing...");
// Fall back to listing existing emails
return await inbox.GetEmailsAsync(cancellationToken);
}
}
[TestFixture]
public class EmailTests
{
private IVaultSandboxClient _client = null!;
private IInbox? _inbox;
[SetUp]
public async Task SetUp()
{
_client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(Environment.GetEnvironmentVariable("VAULTSANDBOX_URL"))
.WithApiKey(Environment.GetEnvironmentVariable("VAULTSANDBOX_API_KEY"))
.Build();
_inbox = await _client.CreateInboxAsync();
}
[TearDown]
public async Task TearDown()
{
// Always clean up, even if test failed
if (_inbox is not null)
{
try
{
await _client.DeleteInboxAsync(_inbox.EmailAddress);
}
catch (InboxNotFoundException)
{
// Inbox already deleted, that's fine
Console.WriteLine("Inbox already deleted");
}
catch (ApiException ex) when (ex.StatusCode == 404)
{
// Also acceptable
Console.WriteLine("Inbox already deleted");
}
catch (Exception ex)
{
// Log but don't fail the test
Console.WriteLine($"Failed to delete inbox: {ex.Message}");
}
}
}
[Test]
public async Task Should_Receive_Email()
{
await SendEmailAsync(_inbox!.EmailAddress);
var email = await _inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(10),
Subject = "Test",
UseRegex = true
});
Assert.That(email.Subject, Does.Contain("Test"));
}
}

Timeouts are common in email testing. Always handle them explicitly:

try
{
var email = await inbox.WaitForEmailAsync(new WaitForEmailOptions
{
Timeout = TimeSpan.FromSeconds(10)
});
}
catch (VaultSandboxTimeoutException)
{
// List what emails did arrive
var emails = await inbox.GetEmailsAsync();
Console.WriteLine($"Expected email not found. Received {emails.Count} emails:");
foreach (var e in emails)
{
Console.WriteLine($" - \"{e.Subject}\" from {e.From}");
}
throw;
}

Always log signature verification and decryption exceptions:

try
{
var inbox = await client.CreateInboxAsync();
}
catch (Exception ex) when (ex is SignatureVerificationException or DecryptionException)
{
// Critical security/integrity exception
_logger.LogCritical(ex, "Security exception occurred. Server: {ServerUrl}, Time: {Timestamp}",
client.Options.BaseUrl,
DateTimeOffset.UtcNow);
// Alert operations team
await AlertOpsAsync(ex);
throw;
}

Catch specific exceptions before generic ones:

// Good: Specific to general
try
{
// ...
}
catch (InboxNotFoundException)
{
// Handle not found case specifically
}
catch (ApiException ex) when (ex.StatusCode == 404)
{
// Handle other 404 cases (email not found)
}
catch (ApiException ex)
{
// Handle other API errors
}
catch (VaultSandboxTimeoutException)
{
// Handle timeout case
}
catch (VaultSandboxException ex)
{
// Handle any other SDK exception
}
catch (Exception ex)
{
// Handle unexpected exceptions
}
// Avoid: Too generic
try
{
// ...
}
catch (VaultSandboxException)
{
// Can't differentiate between exception types
}

Always clean up, even when exceptions occur:

var client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(baseUrl)
.WithApiKey(apiKey)
.Build();
try
{
var inbox = await client.CreateInboxAsync();
// Use inbox...
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
throw;
}
finally
{
if (client is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else if (client is IDisposable disposable)
{
disposable.Dispose();
}
}

Or use await using for automatic disposal:

await using var client = VaultSandboxClientBuilder.Create()
.WithBaseUrl(baseUrl)
.WithApiKey(apiKey)
.Build();
var inbox = await client.CreateInboxAsync();
// Client is automatically disposed when scope exits