Error Handling in Streams
Recovering from Failure
When working with streams that may encounter errors, it’s crucial to know how to handle these errors gracefully. The Stream.catch function is a powerful tool for recovering from failures and switching to an alternative stream in case of an error.
Example
import { Stream, Effect } from "effect"
const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.fail("Oh! Error!")), Stream.concat(Stream.make(4, 5)),)
const s2 = Stream.make("a", "b", "c")
const stream = Stream.catch(s1, () => s2)
await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3, "a", "b", "c"]In this example, s1 encounters an error, but instead of terminating the stream, we gracefully switch to s2 using Stream.catch. This ensures that we can continue processing data even if one stream fails.
You can also distinguish elements from the two streams based on success or failure by tagging each side with the Result data type before merging them, mapping s1’s elements into Result.succeed and s2’s elements into Result.fail:
import { Stream, Effect, Result } from "effect"
const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.fail("Oh! Error!")), Stream.concat(Stream.make(4, 5)),)
const s2 = Stream.make("a", "b", "c")
const stream = Stream.map(s1, Result.succeed).pipe( Stream.catch(() => Stream.map(s2, Result.fail)),)
await Effect.runPromise(Stream.runCollect(stream)) // => [Result.succeed(1), Result.succeed(2), Result.succeed(3), Result.fail("a"), Result.fail("b"), Result.fail("c")]The Stream.catch function provides advanced error handling capabilities compared to Stream.catch. With Stream.catch, you can make decisions based on both the type and value of the encountered failure.
import { Stream, Effect } from "effect"
const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.fail("Uh Oh!" as const)), Stream.concat(Stream.make(4, 5)), Stream.concat(Stream.fail("Ouch" as const)),)
const s2 = Stream.make("a", "b", "c")
const s3 = Stream.make(true, false, false)
const stream = Stream.catch(s1, (error): Stream.Stream<string | boolean> => { switch (error) { case "Uh Oh!": return s2 case "Ouch": return s3 }})
await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3, "a", "b", "c"]In this example, we have a stream, s1, which may encounter two different types of errors. Instead of a straightforward switch to an alternative stream, as done with Stream.catch, we employ Stream.catch to precisely determine how to handle each type of error. This level of control over error recovery enables you to choose different streams or actions based on the specific error conditions.
Recovering from Defects
When working with streams, it’s essential to be prepared for various failure scenarios, including defects that might occur during stream processing. To address this, the Stream.catchCause function provides a robust solution. It enables you to gracefully handle and recover from any type of failure that may arise.
Example
import { Stream, Effect } from "effect"
const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.die(new Error("Boom!"))), Stream.concat(Stream.make(4, 5)),)
const s2 = Stream.make("a", "b", "c")
const stream = Stream.catchCause(s1, () => s2)
await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3, "a", "b", "c"]In this example, s1 may encounter a defect, but instead of crashing the application, we use Stream.catchCause to gracefully switch to an alternative stream, s2. This ensures that your application remains robust and continues processing data even in the face of unexpected issues.
Recovery from Some Errors
In stream processing, there may be situations where you need to recover from specific types of failures. The Stream.catchFilter and Stream.catchCauseFilter functions come to the rescue, allowing you to handle and mitigate errors selectively.
If you want to recover from a particular error, you can use Stream.catchFilter:
import { Stream, Effect, Filter } from "effect"
const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.fail("Oh! Error!")), Stream.concat(Stream.make(4, 5)),)
const s2 = Stream.make("a", "b", "c")
const stream = Stream.catchFilter( s1, Filter.fromPredicate((error) => error === "Oh! Error!"), () => s2,)
await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3, "a", "b", "c"]To recover from a specific cause, you can use the Stream.catchCauseFilter function:
import { Stream, Effect, Cause } from "effect"
const s1 = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.die(new Error("Oh! Error!"))), Stream.concat(Stream.make(4, 5)),)
const s2 = Stream.make("a", "b", "c")
const stream = Stream.catchCauseFilter(s1, Cause.findDie, () => s2)
await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3, "a", "b", "c"]Running Cleanup on Failure
Stream.onError runs an effect when the stream fails, then preserves the original failure. Use it for cleanup or diagnostics, not for recovery.
import { Stream, Console, Effect } from "effect"
const stream = Stream.make(1, 2, 3).pipe( Stream.concat(Stream.die(new Error("Oh! Boom!"))), Stream.concat(Stream.make(4, 5)), Stream.onError(() => Console.log( "Stream application closed! We are doing some cleanup jobs.", ).pipe(Effect.orDie), ),)
Effect.runPromise(Stream.runCollect(stream)).then(console.log)/*Output:Stream application closed! We are doing some cleanup jobs.Error: Oh! Boom!*/Retry a Failing Stream
Sometimes, streams may encounter failures that are temporary or recoverable. In such cases, the Stream.retry operator comes in handy. It allows you to specify a retry schedule, and the stream will be retried according to that schedule.
Example
import { Stream, Effect, Schedule } from "effect"import * as NodeReadLine from "node:readline"
const stream = Stream.make(1, 2, 3).pipe( Stream.concat( Stream.fromEffect( Effect.gen(function* () { const s = yield* readLine("Enter a number: ") const n = parseInt(s) if (Number.isNaN(n)) { return yield* Effect.fail("NaN") } return n }), ).pipe(Stream.retry(Schedule.exponential("1 second"))), ),)
Effect.runPromise(Stream.runCollect(stream)).then(console.log)/*Output:Enter a number: aEnter a number: bEnter a number: cEnter a number: 4[ 1, 2, 3, 4 ]*/
const readLine = (message: string): Effect.Effect<string> => Effect.promise( () => new Promise((resolve) => { const rl = NodeReadLine.createInterface({ input: process.stdin, output: process.stdout, }) rl.question(message, (answer) => { rl.close() resolve(answer) }) }), )In this example, the stream asks the user to input a number, but if an invalid value is entered (e.g., “a,” “b,” “c”), it fails with “NaN.” However, we use Stream.retry with an exponential backoff schedule, which means it will retry after a delay of increasing duration. This allows us to handle temporary errors and eventually collect valid input.
Refining Errors
When working with streams, there might be situations where you want to selectively keep certain errors and terminate the stream with the remaining errors. You can achieve this by pattern matching on the error inside Stream.catch, re-failing with the errors you want to keep and dying (Stream.die) on the rest.
Example
import { Stream, Option, Effect, Exit } from "effect"
const stream = Stream.fail(new Error())
const res = Stream.catch(stream, (error) => { const refined = error instanceof SyntaxError ? Option.some(error) : Option.none() return Option.isSome(refined) ? Stream.fail(refined.value) : Stream.die(error)})
await Effect.runPromiseExit(Stream.runCollect(res)) // => Exit.die(new Error())In this example, stream initially fails with a generic Error. However, res filters and keeps only errors of type SyntaxError, re-failing the stream with them. Any other error is turned into a defect via Stream.die, terminating the stream.
Timing Out
When working with streams, there are scenarios where you may want to handle timeouts, such as terminating a stream if it doesn’t produce a value within a certain duration. In this section, we’ll explore how to manage timeouts using various operators.
timeout
The Stream.timeout operator allows you to set a timeout on a stream. If the stream does not produce a value within the specified duration, it terminates.
import { Stream, Effect } from "effect"
const stream = Stream.fromEffect(Effect.never).pipe(Stream.timeout("2 seconds"))
await Effect.runPromise(Stream.runCollect(stream)) // => []timeoutFail
The Stream.timeoutOrElse operator combines a timeout with a custom failure message. If the stream times out, it fails with the specified error message.
import { Stream, Effect, Exit } from "effect"
const stream = Stream.fromEffect(Effect.never).pipe( Stream.timeoutOrElse({ duration: "2 seconds", orElse: () => Stream.fail("timeout"), }),)
await Effect.runPromiseExit(Stream.runCollect(stream)) // => Exit.fail("timeout")timeoutFailCause
Similar to Stream.timeoutOrElse, Stream.timeoutOrElse combines a timeout with a custom failure cause. If the stream times out, it fails with the specified cause.
import { Stream, Effect, Cause, Exit } from "effect"
const stream = Stream.fromEffect(Effect.never).pipe( Stream.timeoutOrElse({ duration: "2 seconds", orElse: () => Stream.failCause(Cause.die("timeout")), }),)
await Effect.runPromiseExit(Stream.runCollect(stream)) // => Exit.die("timeout")timeoutTo
The Stream.timeoutOrElse operator allows you to switch to another stream if the first stream does not produce a value within the specified duration.
import { Stream, Effect } from "effect"
const stream = Stream.fromEffect(Effect.never).pipe( Stream.timeoutOrElse({ duration: "2 seconds", orElse: () => Stream.make(1, 2, 3), }),)
await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3]