question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Flow error handling and launchIn

See original GitHub issue

Here is a proposal to address the pattern of code that is commonly found in UI code that needs to launch a coroutine collecting a flow and updating UI such that:

uiScope.launch { 
    responseDataFlow().collect { updateDispaly(it) } 
}

launchIn

The first piece is launchIn(scope) operator that launches a coroutine collecting a flow and returns a Job. Hereby I propose that this operator does not take any lambda, so that the replacement for the original code pattern is:

responseDataFlow()
    .onEach { updateDisplay(it) }
    .launchIn(uiScope)

This usage pattern maintains a consistent rule that execution context specifications in flows always work on “upstream” flows (like in flowOn). Another reason for such a syntactic form will be apparent in the next sections.

onError

The most-generic basic handling operator is onError. It catches exception that happens in flow before this operator is applied and pass the caught exception to the supplied lambda. Example:

responseDataFlow()
    .onEach { updateDisplay(it) }
    .onError { e -> showErrorMessage(e) } // catch errors in response flow and updateDisplay
    .launchIn(uiScope)

Notice how onError is written after onEach to resemble the regular try/catch code. Here it is important that collectIn takes no lambda and cannot fail, so writing onError before launchIn always catches all exceptions.

Implementation note: onError operator is already implemented in flow code, but now it is a private function called collectSafely that is used internally to implement other error-handling operators.

onCompletion

This operator calls its lambda whenever a flow completes for any reason, essentially working as try/finally:

responseDataFlow()
    .onEach { updateDisplay(it) }
    .onError { e -> showErrorMessage(e) }
    .onCompletion { enableActionButtons() }
    .launchIn(uiScope)

Advanced error handling

In addition to passing in an exception, onError operator also has FlowCollector receiver, which enables concise encoding of common error-handling patterns to replace an error with a value emitted to the flow. Some of those patterns are already provided as ready-to-use operators:

onErrorCollect(fallback: Flow<T>) = onError { emitAll(fallback) } 
onErrorReturn(fallback: T) = onError { emit(fallback) }

TBD: Shall we rename them to onErrorEmitAll and onErrorEmit or leave them as is?

But onError can be used directly for more advanced case. For example, if there is a flow of some Response objects that support a Response.Failure case that wraps exception, then one can easily translate an exception in the upstream stream to a failed response:

responseDataFlow()
    .onError { e -> emit(Response.Failure(e)) }
    .onEach { updateDisplay(it) } // failure in repose are encoded as values in the flow
    .launchIn(uiScope)

Retry

For consistency I also propose to rename retry to onErrorRetry.

Open questions

  1. Shall onError be configured with (Throwable)->Boolean predicate that defaults to “always true” just like the other onErrorXxx operators? It is not ideal to have a two-lambda function, but predicate here is a “strategy” that might be stored in a separate variable, which actually might result in quite a readable code like onError(applicationErrors) { e -> displayAppErrorMessage(e) }

  2. onError vs onCompletion. One might envision a separate set of onCompletionXxx operators that are similar to onErrorXxx but also perform the corresponding action on the normal completion of the flow, optionally accepting a (Throwable?)->Boolean predicate. It is an open question if there are any use-cases to that.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:23 (14 by maintainers)

github_iconTop GitHub Comments

7reactions
elizarovcommented, Jun 9, 2019

… are we trying to simulate try/catch instead of using it?

Yes. The goal is have more concise and more composable way to do try/catch/finally so that, for example, you can encapsulate error handling logic specific to your application as a flow operator and reuse it everywhere in a declarative way.

Generally, using “bare” try/catch/finally in Kotlin is not very idiomatic. It is kind of “low-level” primitive that you usually find encapsulated inside higher-level operations. So far, coroutines are missing those “high-level” error-handling operations so you have to use try/catch/finally and the resulting code just looks “out-of-place” – a piece of imperative error-handling in sea of declarative code.

4reactions
elizarovcommented, Jun 8, 2019

Is the order in which onError and onEach must be written enforced by onError returning a different type than onEach? Or is there another way the order can be enforced by the compiler.

I don’t see any easy way to enforce an order and I don’t see why it needs to enforced. It’s like try/catch. You catch error in the code in the above try block. We can tweak the naming to make “modifying” nature of onError more explicit. We can even use catch (instead of onError) and finally (instead of onCompletion):

responseDataFlow()
    .onEach { updateDisplay(it) }
    .catch { e -> showErrorMessage(e) }
    .finally { enableActionButtons() }
    .launchIn(uiScope)

(If go with this naming, we need to change the names of all the other onErrorXxx operators for consistency)

Note, that you can use “error catching” operators multiple times to catch errors in differents parts of the flow:

flow
    .catch { e -> ... } // catch flow errors
    .map { first(it) } 
    .catch { e -> ... } // catch first(it) failures
    .map { second(it) } 
    .catch { e -> ... } // catch second(it) failures
    .collect { ... } // do something with elements, can also use launchIn
Read more comments on GitHub >

github_iconTop Results From Across the Web

Exceptions in Kotlin Flows - Roman Elizarov - Medium
But with the flow returned by handleError this exception gets caught and does not appear, so collect call completes normally.
Read more >
From RxJava to Kotlin Flow: Error Handling - ProAndroidDev
Error handling is fundamental in reactive programming. Reactive streams might fail with exception and propagate it as an event downstream ...
Read more >
Customize What Happens When a Flow Fails - Salesforce Help
If your flow contains an element that interacts with the Salesforce database—such as an Update Records element or a Submit for Approval core...
Read more >
Solved: How to Deal with Error Handling when Launching Exc...
Solved: I have a very simple PAD flow that launches excel, opens an xls file and then saves it as xlsx. This works,...
Read more >
Flow error handler - ServiceNow Docs
Enable flows to catch errors. Run a sequence of actions and subflows to identify and correct issues. For example, have flows log output ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found