I’ve recently modelled Scott Wlaschin’s ‘railway-oriented programming’ technique ( https://fsharpforfunandprofit.com/rop/ ) using Akka Streams and have a couple of thoughts.
First, a little motivation. In Akka Streams currently error handling is not very fine-grained. If there’s an exception in any stage, it effectively blows up the remainder of the stream. We can set up custom retry logic at any stage, but the element that caused the error in the first place will be dropped. For finer control, currently the advice is to handle exceptions manually with try
/Try
. E.g., Questions about error handling in Flows
If you’re familiar with ROP already, here’s the gist of it: you can implement it in Akka Streams with Partition
s ( https://doc.akka.io/api/akka/current/akka/stream/scaladsl/Partition$.html
, and optionally a Merge
if you want to feed all errors into a single sink). The key is to wrap the output of a flow in a result type like Try
or Either
so you can feed it into a Partition
and split the successes and failures into separate streams. Here’s an example:
parseJson ~> partitionParseJson.in
// In the Unix tradition, output 0 is success and 1 is failure
partitionParseJson.out(0) ~> successParseJson
partitionParseJson.out(1) ~> failureParseJson
parseJson
can e.g. take a String
input to an Either[Error, Json]
like Circe does. partitionParseJson
can look like:
val partitionParseJson = builder.add(Partition[Either[Error, Json]](2, {
case Right(_) => 0
case Left(_) => 1
}))
Once they get the partitioned outputs from the previous stage, successParseJson
and failureParseJson
will want to extract the actual values so that subsequent stages can work with them:
val successParseJson = Flow[Either[Error, Json]].collect {
case Right(json) => json
}
In this flow we don’t care about Left
values because we know it will only get Right
values at runtime, and vice-versa for failureParseJson
.
The upshot is that we can set up a literal happy path and sad paths using partitions and propagate successes and errors along them.