Table of Contents

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:

  1. Platform-managed retries - Use platform retry settings (configured by the user)
  2. 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.

>> Module Status