From 757c2ce0045719069ec1818aa7f745081cd8368c Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Wed, 19 Nov 2025 14:01:51 -0500 Subject: [PATCH 1/7] add AsyncNodePlugin API Updates rfc --- player/0000-AsyncNodePlugin-API-Updates.md | 154 +++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 player/0000-AsyncNodePlugin-API-Updates.md diff --git a/player/0000-AsyncNodePlugin-API-Updates.md b/player/0000-AsyncNodePlugin-API-Updates.md new file mode 100644 index 0000000..bfaa309 --- /dev/null +++ b/player/0000-AsyncNodePlugin-API-Updates.md @@ -0,0 +1,154 @@ +# Overview +[overview]: #overview + +- Name: AsyncNodePlugin API Updates +- Platform (Core, React, JVM, Android, iOS): Core +- Date: + +# Summary +[summary]: #summary + +The goal of this RFC is to remove the `AsyncNodePluginPlugin`, update the `onAsyncNode` hook to take a handler object rather than a promise of future content, and to change the node `update` function to provide options that allow for better optimizations around lists of content. + + + +# Motivation +[motivation]: #motivationz + +There are 4 main motivations with these changes: +1. Simplify setup and make any plugin needed for streaming capabilities have a clear purpose. +2. Make interacting with async nodes intuitive, removing confusing promise management as a step for managing control over individual nodes. +3. Introduce an API for dealing with async content that we can more easily make changes to. Minimize the need for breaking changes in the future and simplify the deprecation path. +4. Make managing lists of async content not require inserting many async nodes by providing better optimizations around parsing and resolving changing lists. + + + +# Design + +## Removing the AsyncNodePluginPlguin + +`AsyncNodePluginPlugin` currently provides most of the features a user would expect from the main `AsyncNodePlugin`. It ends up calling the hooks on `AsyncNodePlugin`, which keeps the features of both tightly coupled to each other. The name has also been a confusing point for users as they expect the `AsyncNodePluginPlugin` to provide some shortcuts to tapping into the `AsyncNodePlugin` to help with managing async nodes. + +To simplify this and reduce setup steps, the `AsyncNodePluginPlugin` class can be removed and its features can be moved to `AsyncNodePlugin` so that there are no references across plugins. + +`AsyncNodePlugin` can have its list of plugins removed for now to reduce options in the constructor. + +## Changes to the AsyncNodePlugin Hooks + +The core design here would be to remove the `onAsyncNodeError` hook, and update `onAsyncNode` to something like: +```ts + +/** function type for updating async content. This type matches the current update of `onAsyncNode` but will come up in a later stage for how it can be changed as well. */ +export type ContentUpdateFunction = (content: any) => void; + +export interface AsyncNodeHandler { + /** Start function for the handler. Used once on setup to pass the handler the update function and notify it that the node is ready for updates so it can start any async process. */ + start: (node: Node.Async, updateFunction: ContentUpdateFunction) => void; + /** Function called during the first pass of the resolver, when an async node is initially identified. Setup any placeholder content here. */ + getInitialState?: (node: Node.Async) => any; + /** Handler for errors during an update triggered by an async node updating */ + onError?: (node: Node.Async, error: Error) => void +} + +export type AsyncNodePluginHooks = { + /** hook to manage handlers for individual async nodes */ + onAsyncNode: SyncBailHook<[Node.Async], AsyncNodeHandler>; +} +``` + +In this case, the `onAsyncNode` hook has been changed to be a `Sync` hook rather than an `Async` hook. This allows for handlers for each async node to be determined synchronously and gives us the ability to allow for an initial state of the async content without needing to trigger an additional update of the view. This also reduces the burden of promise management on users. Currently in order to ensure only your own plugin is managing a specific node, plugins must manage promises that aren't meant to resolve and call the update function as needed after that. This also allows us to reuse the same `onAsyncNode` hook as we make changes to what the `AsyncNodeHandler` type has down the line. + +## Changing the async node `update` function + +Updating an async node with new content causes the `AsyncNodePlugin` to parse the new content and produce new AST nodes. This works well for simple use cases, but makes managing lists of content difficult. To minimize perf issues, users must use async nodes for each new piece of content inserted into the list. Consider a case of messages being streamed into a collection. If a single async node is used to manage all content inserted into the collection, the time it takes to parse and update the view will increase with each new message. To minimize this impact, users need to insert a new async node for every message which makes certain list operations difficult. + +To fix this, replacing the `update` function with an `updater` object that supports some list-like operations for updating a single async node: + +```ts +export interface AsyncNodeUpdater { + /** Sets the content for the async node, replacing any existing content. This works exactly as `update` does now. */ + set: (content: any) => void; + /** Removes any content associated with the async node. */ + delete: () => void; + /** Gets the current content */ + getCurrentContent: () => any; + + /** The following list-like operations only work if the content is an array. Throws an error otherwise. */ + + /** Pushes the content to the end of the array. */ + push: (...content: any[]) => void; + /** Inserts all content at the index. */ + insert: (index: number, ...content: any[]) => void; + /** Replaces existing content at an index. Throws if index out of bounds. */ + replaceAt: (index: number, ...content: any[]) => void; + /** Removes the item at the index. Throws if index out of bounds */ + removeAt: (index: number) => void; +} +``` + +Using these operations, the `AsyncNodePlugin` will provide an object that can track changes and ensure AST nodes are properly cached and interact as intended with the view resolver's cache while only maintaining a single node. + + + +# Risks +[risks]: #risks + +- The sync hook approach loses the ability for multiple handlers to exist for a single async node. An alternative here to allow that would be to use a `SyncHook` that provides a `registerHandler` function, but allowing for multiple handlers might make it confusing when and how a specific node is getting updated. +- Managing list operations on the `AsyncNodeUpdater` is always going to be more limited than managing an actual list and will likely require additional support with new options as new use cases come up. + + + + +# Unknowns +[unknowns]: #unknowns + +## AsyncNodeUpdater + +- How do we batch operations to prevent multiple updates from being triggered at once? +- Do we want to handle `undefined` content as an empty list when using `push` instead of throwing? +- Should the list operations include options that allow for iterating through multiple items? (ex. map, filter, etc.) +- Should the list operations allow for something like splice or slice to remove many items at once? + +### How do we avoid breaking changes? + +The changes above, if made directly as suggested, will cause breaking changes in the async node package. To cover for this there are a couple approaches: + +#### 1. New Plugin + +Instead of changing anything in the existing plugins, mark those as deprecated and make a new plugin within the same package. This is likely to cause some code duplication for now, but does the best job of isolating the change. + +#### 2. Change within the plugins + +If we want the existing plugins to work with any new changes without having users migrate immediately, this will be more convenient. To support this there are a few things that will need to be done: + +- Instead of deleting `AsyncNodePluginPlugin`, mark it as deprecated. Still move all the code to `AsyncNodePlugin` so this essentially ends up being an empty class. +- Do not remove the existing hooks. Add the new API as a new `asyncHandler` hook in addition to the existing hooks. Mark the old hooks as deprecated. +- Mark the existing constructor of `AsyncNodePlugin` as deprecated in favour of something without the `plugins` array argument. + +To deal with the overlap between the new `asyncHandler` hook and the `onAsyncNode` and `onAsyncNodeError` hooks, any time one would be called, we should prefer using the result from `asyncHandler` first. + +For example, when an async node is first identified and we need a handler, call `asyncHandler` first, and only call `onAsyncNode` if that does not return a result. If an error occurs, check the handler for an `onError` function before calling `onAsyncNodeError` to handle it. + + + +# Alternatives Considered +[alternatives-considered]: #alternatives-considered + +## AsyncNodeUpdater +Alternatively to updating using content, we could require that users provide the AST themselves when updating an async node, providing the parse function and other necessary utils so they can still deal with regular json content coming from a service. This would let users keep a single multi-node that they update if they are trying to manipulate an array without the need for us to introduce functions for dealing with all of that. In this way we would still have an update function that looks more like: + +```ts +export type AsyncNodeUpdateFunction = (node: Node.Node) => void; +``` + +Calling this would invalidate the resolver cache for the node at the top level, but for any multi-nodes or nodes with children this would preserve the cache for nested nodes as long as object references are maintained. + +This approach will create more discrepancies between the existing API and the updated API, and puts more burden on the users to understand how player's resolver cache interacts with any nodes they are adding or changing. If this is something we document well and can expect users to manage, than this simplifies the work needed on the plugin side and reduces the likelihood of needing future updates to support more list operations. + + + +# Unlocks +[unlocks]: #unlocks + +N/A + \ No newline at end of file From 097217c54adb88c67fc3d50e1d5ee3cd949cda17 Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Wed, 19 Nov 2025 14:03:06 -0500 Subject: [PATCH 2/7] small text updates --- player/0000-AsyncNodePlugin-API-Updates.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/player/0000-AsyncNodePlugin-API-Updates.md b/player/0000-AsyncNodePlugin-API-Updates.md index bfaa309..bc6cb80 100644 --- a/player/0000-AsyncNodePlugin-API-Updates.md +++ b/player/0000-AsyncNodePlugin-API-Updates.md @@ -31,7 +31,7 @@ There are 4 main motivations with these changes: To simplify this and reduce setup steps, the `AsyncNodePluginPlugin` class can be removed and its features can be moved to `AsyncNodePlugin` so that there are no references across plugins. -`AsyncNodePlugin` can have its list of plugins removed for now to reduce options in the constructor. +`AsyncNodePlugin` can have its list of plugins removed to reduce options in the constructor. ## Changes to the AsyncNodePlugin Hooks @@ -111,7 +111,7 @@ Using these operations, the `AsyncNodePlugin` will provide an object that can tr ### How do we avoid breaking changes? -The changes above, if made directly as suggested, will cause breaking changes in the async node package. To cover for this there are a couple approaches: +The changes in this RFC, if made directly as suggested, will cause breaking changes in the async node package. To cover for this there are a couple approaches: #### 1. New Plugin From d27248bb967e9189a52ca2a053cfa88d82b0c80c Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Fri, 9 Jan 2026 10:36:25 -0500 Subject: [PATCH 3/7] remove async node update function changes from rfc --- player/0000-AsyncNodePlugin-API-Updates.md | 58 ++++------------------ 1 file changed, 9 insertions(+), 49 deletions(-) diff --git a/player/0000-AsyncNodePlugin-API-Updates.md b/player/0000-AsyncNodePlugin-API-Updates.md index bc6cb80..2803952 100644 --- a/player/0000-AsyncNodePlugin-API-Updates.md +++ b/player/0000-AsyncNodePlugin-API-Updates.md @@ -38,7 +38,7 @@ To simplify this and reduce setup steps, the `AsyncNodePluginPlugin` class can b The core design here would be to remove the `onAsyncNodeError` hook, and update `onAsyncNode` to something like: ```ts -/** function type for updating async content. This type matches the current update of `onAsyncNode` but will come up in a later stage for how it can be changed as well. */ +/** function type for updating async content. This type matches the current update of `onAsyncNode`. */ export type ContentUpdateFunction = (content: any) => void; export interface AsyncNodeHandler { @@ -58,36 +58,6 @@ export type AsyncNodePluginHooks = { In this case, the `onAsyncNode` hook has been changed to be a `Sync` hook rather than an `Async` hook. This allows for handlers for each async node to be determined synchronously and gives us the ability to allow for an initial state of the async content without needing to trigger an additional update of the view. This also reduces the burden of promise management on users. Currently in order to ensure only your own plugin is managing a specific node, plugins must manage promises that aren't meant to resolve and call the update function as needed after that. This also allows us to reuse the same `onAsyncNode` hook as we make changes to what the `AsyncNodeHandler` type has down the line. -## Changing the async node `update` function - -Updating an async node with new content causes the `AsyncNodePlugin` to parse the new content and produce new AST nodes. This works well for simple use cases, but makes managing lists of content difficult. To minimize perf issues, users must use async nodes for each new piece of content inserted into the list. Consider a case of messages being streamed into a collection. If a single async node is used to manage all content inserted into the collection, the time it takes to parse and update the view will increase with each new message. To minimize this impact, users need to insert a new async node for every message which makes certain list operations difficult. - -To fix this, replacing the `update` function with an `updater` object that supports some list-like operations for updating a single async node: - -```ts -export interface AsyncNodeUpdater { - /** Sets the content for the async node, replacing any existing content. This works exactly as `update` does now. */ - set: (content: any) => void; - /** Removes any content associated with the async node. */ - delete: () => void; - /** Gets the current content */ - getCurrentContent: () => any; - - /** The following list-like operations only work if the content is an array. Throws an error otherwise. */ - - /** Pushes the content to the end of the array. */ - push: (...content: any[]) => void; - /** Inserts all content at the index. */ - insert: (index: number, ...content: any[]) => void; - /** Replaces existing content at an index. Throws if index out of bounds. */ - replaceAt: (index: number, ...content: any[]) => void; - /** Removes the item at the index. Throws if index out of bounds */ - removeAt: (index: number) => void; -} -``` - -Using these operations, the `AsyncNodePlugin` will provide an object that can track changes and ensure AST nodes are properly cached and interact as intended with the view resolver's cache while only maintaining a single node. - # Risks @@ -102,13 +72,6 @@ Using these operations, the `AsyncNodePlugin` will provide an object that can tr # Unknowns [unknowns]: #unknowns -## AsyncNodeUpdater - -- How do we batch operations to prevent multiple updates from being triggered at once? -- Do we want to handle `undefined` content as an empty list when using `push` instead of throwing? -- Should the list operations include options that allow for iterating through multiple items? (ex. map, filter, etc.) -- Should the list operations allow for something like splice or slice to remove many items at once? - ### How do we avoid breaking changes? The changes in this RFC, if made directly as suggested, will cause breaking changes in the async node package. To cover for this there are a couple approaches: @@ -134,16 +97,7 @@ For example, when an async node is first identified and we need a handler, call # Alternatives Considered [alternatives-considered]: #alternatives-considered -## AsyncNodeUpdater -Alternatively to updating using content, we could require that users provide the AST themselves when updating an async node, providing the parse function and other necessary utils so they can still deal with regular json content coming from a service. This would let users keep a single multi-node that they update if they are trying to manipulate an array without the need for us to introduce functions for dealing with all of that. In this way we would still have an update function that looks more like: - -```ts -export type AsyncNodeUpdateFunction = (node: Node.Node) => void; -``` - -Calling this would invalidate the resolver cache for the node at the top level, but for any multi-nodes or nodes with children this would preserve the cache for nested nodes as long as object references are maintained. - -This approach will create more discrepancies between the existing API and the updated API, and puts more burden on the users to understand how player's resolver cache interacts with any nodes they are adding or changing. If this is something we document well and can expect users to manage, than this simplifies the work needed on the plugin side and reduces the likelihood of needing future updates to support more list operations. +N/A @@ -151,4 +105,10 @@ This approach will create more discrepancies between the existing API and the up [unlocks]: #unlocks N/A - \ No newline at end of file + + +# Out of Scope + +## Changing the async node `update` function + +There is a lot to consider with this topic and we need more information on how users are using async capabilities before we push for a new implementation on this update function. \ No newline at end of file From 9930ccb7c86984a833dbd571d0aff1b45d0fdc21 Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Mon, 12 Jan 2026 12:40:57 -0500 Subject: [PATCH 4/7] Update player/0000-AsyncNodePlugin-API-Updates.md Co-authored-by: Ketan Reddy --- player/0000-AsyncNodePlugin-API-Updates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/0000-AsyncNodePlugin-API-Updates.md b/player/0000-AsyncNodePlugin-API-Updates.md index 2803952..604f842 100644 --- a/player/0000-AsyncNodePlugin-API-Updates.md +++ b/player/0000-AsyncNodePlugin-API-Updates.md @@ -13,7 +13,7 @@ The goal of this RFC is to remove the `AsyncNodePluginPlugin`, update the `onAsy # Motivation -[motivation]: #motivationz +[motivation]: #motivation There are 4 main motivations with these changes: 1. Simplify setup and make any plugin needed for streaming capabilities have a clear purpose. From f691363d5f87dcf1ad99941c5f905f374e261399 Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Tue, 13 Jan 2026 11:38:21 -0500 Subject: [PATCH 5/7] describe design decision around synchook. fix typo in template --- player/0000-AsyncNodePlugin-API-Updates.md | 26 ++++++++++++++++++---- player/0000-template.md | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/player/0000-AsyncNodePlugin-API-Updates.md b/player/0000-AsyncNodePlugin-API-Updates.md index 604f842..ae19fea 100644 --- a/player/0000-AsyncNodePlugin-API-Updates.md +++ b/player/0000-AsyncNodePlugin-API-Updates.md @@ -15,11 +15,10 @@ The goal of this RFC is to remove the `AsyncNodePluginPlugin`, update the `onAsy # Motivation [motivation]: #motivation -There are 4 main motivations with these changes: +There are 3 main motivations with these changes: 1. Simplify setup and make any plugin needed for streaming capabilities have a clear purpose. 2. Make interacting with async nodes intuitive, removing confusing promise management as a step for managing control over individual nodes. 3. Introduce an API for dealing with async content that we can more easily make changes to. Minimize the need for breaking changes in the future and simplify the deprecation path. -4. Make managing lists of async content not require inserting many async nodes by providing better optimizations around parsing and resolving changing lists. @@ -56,7 +55,27 @@ export type AsyncNodePluginHooks = { } ``` -In this case, the `onAsyncNode` hook has been changed to be a `Sync` hook rather than an `Async` hook. This allows for handlers for each async node to be determined synchronously and gives us the ability to allow for an initial state of the async content without needing to trigger an additional update of the view. This also reduces the burden of promise management on users. Currently in order to ensure only your own plugin is managing a specific node, plugins must manage promises that aren't meant to resolve and call the update function as needed after that. This also allows us to reuse the same `onAsyncNode` hook as we make changes to what the `AsyncNodeHandler` type has down the line. +There are some key differences between the existing `AsyncNodePlugin` and this approach. The following sections compare the differencces and explain the intent behind the new design: + +### Sync vs Async hooks + +This approach uses a `SyncBailHook` as opposed to the `AsyncSeriesBailHook` or `AsyncParallelBailHook` used previously. There were a few issues with these hooks. First, there is no way to differentiate in the existing hooks whether something returns `undefined` to declare that it has no intention of updating the async node versus returning `undefined` to explicitly render nothing in place of the async node. This is especially a problem with the `AsyncSeriesBailHook` as knowing when it can or should bail with multiple taps is difficult. Additionally the `AsyncParallelBailHook` is built on `Promise.race` which could lead to incosistent behaviour if multiple handlers exist for a specific node. + +With the `SyncBailHook` the intent is for plugin authors to first declare their intent to manage a specific async node. Returning `undefined` declares that they will not be handling it, and returning an `AsyncNodeHandler` gives us the object with the functions needed to actually manage the content in the async node. + +This does not remove the ability to do async operations. The hook itself is synchronous, but the intent here is that any async functionality is moved to the `start` of the `AysncNodeHandler`. + +### No Promise results + +The current `AsyncNodePlugin` allows for two approaches to updating your content and the way both interact with each other is not always clear. Returning a `Promise` that resolves to the content you want to display or using the `update` function that takes the same object. While using either works, it's also possible to use the `update` after the promise has resolved or rejected. There are no guidelines around when each is appropriate. + +The `Promise` result also has issues with no having a clear way of how to end it without updating the view. Resolving with `undefined` will clear the async content so there is no clear way to complete and cleanup the task. + +To simplify, reducing this to just using the `update` function creates one clear path to updating content while maintaining the flexibility of having any number of updates to a single async node. + +### Single object vs multiple hooks + +Having a single hook that returns a handler allows all related functionality for that async node to be grouped together. While technically more flexible to allow multiple hooks where each tackles one of the given functions (i.e. initial state, start, error) it is reasonable to expect that in most use cases, plugin authors will be trying to manage all aspects of an async node rather than delegating specific responsibilities to other plugins that may not have the context of any requests made to populate the node in the first place. @@ -64,7 +83,6 @@ In this case, the `onAsyncNode` hook has been changed to be a `Sync` hook rather [risks]: #risks - The sync hook approach loses the ability for multiple handlers to exist for a single async node. An alternative here to allow that would be to use a `SyncHook` that provides a `registerHandler` function, but allowing for multiple handlers might make it confusing when and how a specific node is getting updated. -- Managing list operations on the `AsyncNodeUpdater` is always going to be more limited than managing an actual list and will likely require additional support with new options as new use cases come up. diff --git a/player/0000-template.md b/player/0000-template.md index 5331ec3..18b2277 100644 --- a/player/0000-template.md +++ b/player/0000-template.md @@ -11,7 +11,7 @@ # Motivation -[motivation]: #motivationz +[motivation]: #motivation From e6c3cbf621cdd8c5eafc25ac5e555bb48cd5ea4e Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Tue, 13 Jan 2026 11:40:34 -0500 Subject: [PATCH 6/7] rename accepted rfc --- player/{0000-async-node-caching.md => 0001-async-node-caching.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename player/{0000-async-node-caching.md => 0001-async-node-caching.md} (100%) diff --git a/player/0000-async-node-caching.md b/player/0001-async-node-caching.md similarity index 100% rename from player/0000-async-node-caching.md rename to player/0001-async-node-caching.md From 215c81611e1f2e64ffd9b1fc14c7b552eecd052d Mon Sep 17 00:00:00 2001 From: Thomas Marmer Date: Thu, 15 Jan 2026 14:36:09 -0500 Subject: [PATCH 7/7] add approach for avoiding breaking changes --- player/0000-AsyncNodePlugin-API-Updates.md | 24 ++++------------------ 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/player/0000-AsyncNodePlugin-API-Updates.md b/player/0000-AsyncNodePlugin-API-Updates.md index ae19fea..9acf55f 100644 --- a/player/0000-AsyncNodePlugin-API-Updates.md +++ b/player/0000-AsyncNodePlugin-API-Updates.md @@ -24,6 +24,10 @@ There are 3 main motivations with these changes: # Design +## Avoiding breaking changes + +The following updates are going to involve breaking changes when compared to the existing API. To mitigate this, the new hooks and setup should be included as a separate plugin, marked as experimental until we are ready to commit to the new approach and can deprecate and eventually remove the old plugin. This new plugin should be included in the same package as the existing `AsyncNodePlugin` + ## Removing the AsyncNodePluginPlguin `AsyncNodePluginPlugin` currently provides most of the features a user would expect from the main `AsyncNodePlugin`. It ends up calling the hooks on `AsyncNodePlugin`, which keeps the features of both tightly coupled to each other. The name has also been a confusing point for users as they expect the `AsyncNodePluginPlugin` to provide some shortcuts to tapping into the `AsyncNodePlugin` to help with managing async nodes. @@ -90,26 +94,6 @@ Having a single hook that returns a handler allows all related functionality for # Unknowns [unknowns]: #unknowns -### How do we avoid breaking changes? - -The changes in this RFC, if made directly as suggested, will cause breaking changes in the async node package. To cover for this there are a couple approaches: - -#### 1. New Plugin - -Instead of changing anything in the existing plugins, mark those as deprecated and make a new plugin within the same package. This is likely to cause some code duplication for now, but does the best job of isolating the change. - -#### 2. Change within the plugins - -If we want the existing plugins to work with any new changes without having users migrate immediately, this will be more convenient. To support this there are a few things that will need to be done: - -- Instead of deleting `AsyncNodePluginPlugin`, mark it as deprecated. Still move all the code to `AsyncNodePlugin` so this essentially ends up being an empty class. -- Do not remove the existing hooks. Add the new API as a new `asyncHandler` hook in addition to the existing hooks. Mark the old hooks as deprecated. -- Mark the existing constructor of `AsyncNodePlugin` as deprecated in favour of something without the `plugins` array argument. - -To deal with the overlap between the new `asyncHandler` hook and the `onAsyncNode` and `onAsyncNodeError` hooks, any time one would be called, we should prefer using the result from `asyncHandler` first. - -For example, when an async node is first identified and we need a handler, call `asyncHandler` first, and only call `onAsyncNode` if that does not return a result. If an error occurs, check the handler for an `onError` function before calling `onAsyncNodeError` to handle it. - # Alternatives Considered