Error Handling
Since your module will receive a FlowMessage and this is a dynamic object you can´t be sure that the person building the flow will send in the data your module expects. Design modules to handle unexpected or missing data gracefully (use safe getters and ignore unexpected data).
General Principles
- Be fault-tolerant: Don't crash on invalid data
- Validate inputs: Check for required properties before processing
- Use SetStatus: Alert users about issues without stopping the flow (see Module Status)
- Throw only for critical errors: Use FlowModuleException for unrecoverable errors
Retry Strategies
There are two approaches for handling transient errors:
- Platform-managed retries - Use platform retry settings (configured by the user)
- Module-level retries - Implement custom retry logic within your module
The recommended approach is to use platform-managed retries when possible since retries will affect other modules in the flow. Sometimes though you might need to implement custom retry logic within your module, for example when running input modules that read from external services or having timer based logic in the module.
Platform-Managed Retries
The platform provides retry settings that users can configure. When using platform retries, always forward messages and let the platform handle retries:
protected override async Task MessageReceived(IFlowMessage message)
{
try
{
// Attempt processing
await this.ProcessMessage(message);
}
catch (HttpRequestException ex) when (this.IsTransientError(ex))
{
// Log transient error but don't retry here
Warning(ex, "Transient HTTP error occurred");
var error = $"Transient error: {ex.Message}";
this.SetStatus(Status.Warning, error);
message.SetError(error);
// Platform will retry based on user's retry settings
}
catch (Exception ex)
{
// Log permanent errors
Error(ex, "Processing failed");
var error = $"Processing failed: {ex.Message}";
this.SetStatus(Status.Warning, error);
message.SetError(error);
}
finally
{
// Always forward the message - platform handles retries
await this.Next(message);
}
}
private bool IsTransientError(Exception ex)
{
return ex is HttpRequestException httpEx &&
(httpEx.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) ||
httpEx.Message.Contains("connection", StringComparison.OrdinalIgnoreCase));
}
Tip
For transient failures (network, service unavailable), retry logic is implemented for all modules by the SDK. It can be configured in common settings in the UI when the flow is designed. See Messages Queues and Retries for more information.
Module-Level Retries
In general. the platform managed retries should be preferred. However, there are scenarios where you might need custom retry logic within your module. An example is when you have timer-based input modules that read from external services.
public class MyInputModule : InputModule<MySettings>
{
public override async Task<IError?> Start()
{
this.timer = new Timer(async _ => await GenerateData(), null,
TimeSpan.Zero, TimeSpan.FromSeconds(this.Settings.Interval));
return await base.Start();
}
private async Task GenerateData()
{
var message = new FlowMessage();
int maxRetries = 5;
int delayMs = 500;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
var value = await GetSensorReading();
message.Set("value", value);
break;
}
catch (Exception ex)
{
if (attempt == maxRetries)
{
message.SetError($"Sensor read failed after {maxRetries} attempts: {ex.Message}");
}
else
{
await Task.Delay(delayMs);
delayMs *= 2; // Backoff
}
}
}
await this.Next(message);
}
public override void Stop()
{
this.timer?.Dispose();
this.timer = null;
}
}
## Output Modules Need Special Care
Since output modules represent some kind of communication with the outside world things can go wrong. This means that you need to think about always sending a result out from your output modules.
### Example
Let´s say that you create a module that sends a message to one of the services on Azure. At some point the service might be down or your device might have a failing internet connection. To overcome this possible issue you need to make sure that you always pass a message to the next module and that this message contains the result of the modules operation.
```csharp
protected override async Task MessageReceived(IFlowMessage message)
{
try
{
// Validate input
if (!message.Has<string>("deviceId"))
{
// Handle missing deviceId gracefully
this.SetStatus(Status.Warning, "Missing deviceId, skipping message");
return;
}
// Perform logic operation
var isSuccess = await SendToExternalService(message);
if (isSuccess)
{
// If the operation was a success
message.SetSuccess();
}
else
{
// If the operation failed
message.SetError("Failed to send message");
}
}
catch (Exception ex)
{
// Set error on exception
message.SetError(ex.Message);
}
finally
{
// Always forward the message
await this.Next(message);
}
}
[TIP]
message.SetSuccess() can be omitted. The runtime will set success automatically if no error is set.
By using the logic above your module will always output the result. The methods SetSuccess and SetError takes care of writing a crosser object to the message. For example, if you call message.SetSuccess() your message will look like:
{
"crosser": {
"success": true
}
}
If you call message.SetError("Failed to connect to API"), your message will look like:
{
"crosser": {
"success": false,
"message": "Failed to connect to API"
}
}
Tip
For transient failures (network, service unavailable), retry logic is implemented for all modules by the SDK. It can be configured in common settings in the UI when the flow is designed. See Messages Queues and Retries for more information.
Related Topics
- Security Best Practices - Secure error handling principles
- Performance Optimization - Efficient error handling patterns
- External Integrations - Error handling for external services