Skip to content

add render-time and async node error recovery pattern#802

Open
tmarmer wants to merge 35 commits intoerror-controllerfrom
render-error-handling
Open

add render-time and async node error recovery pattern#802
tmarmer wants to merge 35 commits intoerror-controllerfrom
render-error-handling

Conversation

@tmarmer
Copy link
Contributor

@tmarmer tmarmer commented Feb 12, 2026

  • Update captureError to fail the player state if no error transition is available.
  • Implement render-time error recovery functionality
    • iOS: Errors are handled during deserialization of asset data. Asset render functions do not support throwing so actual render-time issues do not need to be caught
    • Android: Errors are caught for each asset type and bubble up according to their needs.
      • RenderableAsset: Bubbles up to PlayerFragment error handling which calls PlayerViewModel.fail which now uses the error controller.
      • SuspendableAsset: Errors caught in the coroutine exception handler are sent to the error controller (except for cancellation exceptions)
      • ComposableAsset: Errors thrown during deserialization are sent to the error controller.
    • React: Top level ErrorBoundary on react player catches and sends errors to the ErrorController.
  • Add PlayerErrorMetadata interface for errors to implement to include info required for the error controller to capture errors
  • Add errors for specific areas where it can be brought to the error controller
    • AssetRenderError updated to include metadata required for the error controller
    • ResolverError added for errors thrown during computeTree
  • Update async-node plugin to make use of the above to call its onAsyncNodeError hook and replace broken content

Change Type (required)

Indicate the type of change your pull request is:

  • patch
  • minor
  • major
  • N/A

Does your PR have any documentation updates?

  • Updated docs
  • No Update needed
  • Unable to update docs

Release Notes

  • Update AsyncNodePlugin's onAsyncNodeError hook to depend on the ErrorController's onError hook for finding errors.
  • Capture render time errors using ErrorController.captureError and allow for error recovery on all platforms.
  • Add PlayerErrorMetadata interface across platforms and ensure serialization of errors keeps additional metadata.
    • When throwing an error that matches this interface, the ErrorController.captureError will prefer data from the error object when calling the onError hook.
📦 Published PR as canary version: 0.15.1--canary.802.31569

Try this version out locally by upgrading relevant packages to 0.15.1--canary.802.31569

@tmarmer tmarmer requested review from a team as code owners February 12, 2026 22:16
@codecov
Copy link

codecov bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 94.38356% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.36%. Comparing base (07bcf2a) to head (107cbfe).

Files with missing lines Patch % Lines
.../com/intuit/playerui/core/error/ErrorController.kt 66.66% 2 Missing and 5 partials ⚠️
plugins/async-node/core/src/index.ts 95.39% 7 Missing ⚠️
...e/serialization/serializers/ThrowableSerializer.kt 76.00% 3 Missing and 3 partials ⚠️
core/player/src/view/resolver/index.ts 93.65% 4 Missing ⚠️
react/player/src/player.tsx 95.87% 4 Missing ⚠️
.../com/intuit/playerui/core/player/HeadlessPlayer.kt 0.00% 3 Missing ⚠️
...e-assets/core/src/plugins/error-recovery-plugin.ts 90.32% 3 Missing ⚠️
core/player/src/controllers/view/controller.ts 87.50% 2 Missing ⚠️
...gins/async-node/core/src/utils/getNodeFromError.ts 91.30% 2 Missing ⚠️
core/player/src/controllers/error/controller.ts 95.83% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@                 Coverage Diff                  @@
##           error-controller     #802      +/-   ##
====================================================
+ Coverage             86.02%   86.36%   +0.33%     
====================================================
  Files                   513      512       -1     
  Lines                 23566    24020     +454     
  Branches               2703     2828     +125     
====================================================
+ Hits                  20272    20744     +472     
+ Misses                 2956     2942      -14     
+ Partials                338      334       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codecov
Copy link

codecov bot commented Feb 13, 2026

Bundle Report

Changes will increase total bundle size by 125.88kB (2.17%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
plugins/beacon/core 422.87kB 5.4kB (1.29%) ⬆️
plugins/check-path/core 441.27kB 5.4kB (1.24%) ⬆️
plugins/stage-revert-data/core 404.89kB 5.4kB (1.35%) ⬆️
react/subscribe 13.68kB 2.04kB (17.56%) ⬆️
plugins/reference-assets/core 503.98kB 25.28kB (5.28%) ⬆️
react/player 84.12kB 10.77kB (14.69%) ⬆️
plugins/common-expressions/core 427.12kB 5.4kB (1.28%) ⬆️
plugins/markdown/core 681.95kB 5.41kB (0.8%) ⬆️
plugins/common-types/core 501.45kB 5.4kB (1.09%) ⬆️
plugins/metrics/core 459.43kB 5.4kB (1.19%) ⬆️
plugins/async-node/core 503.08kB 29.55kB (6.24%) ⬆️
core/player 1.02MB 20.4kB (2.04%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: plugins/check-path/core

Assets Changed:

Asset Name Size Change Total Size Change (%)
CheckPathPlugin.native.js 5.4kB 411.78kB 1.33%
view changes for bundle: plugins/metrics/core

Assets Changed:

Asset Name Size Change Total Size Change (%)
MetricsPlugin.native.js 5.4kB 427.08kB 1.28%
view changes for bundle: react/subscribe

Assets Changed:

Asset Name Size Change Total Size Change (%)
cjs/index.cjs 739 bytes 5.74kB 14.77% ⚠️
index.legacy-esm.js 652 bytes 3.97kB 19.66% ⚠️
index.mjs 652 bytes 3.97kB 19.66% ⚠️
view changes for bundle: plugins/beacon/core

Assets Changed:

Asset Name Size Change Total Size Change (%)
BeaconPlugin.native.js 5.4kB 408.4kB 1.34%
view changes for bundle: plugins/markdown/core

Assets Changed:

Asset Name Size Change Total Size Change (%)
MarkdownPlugin.native.js 5.41kB 656.8kB 0.83%
view changes for bundle: react/player

Assets Changed:

Asset Name Size Change Total Size Change (%)
cjs/index.cjs 3.77kB 30.48kB 14.13% ⚠️
index.legacy-esm.js 3.5kB 26.82kB 15.0% ⚠️
index.mjs 3.5kB 26.82kB 15.0% ⚠️
view changes for bundle: core/player

Assets Changed:

Asset Name Size Change Total Size Change (%)
Player.native.js 6.33kB 425.01kB 1.51%
cjs/index.cjs 4.78kB 202.64kB 2.42%
index.legacy-esm.js 4.65kB 195.91kB 2.43%
index.mjs 4.65kB 195.91kB 2.43%
view changes for bundle: plugins/stage-revert-data/core

Assets Changed:

Asset Name Size Change Total Size Change (%)
StageRevertDataPlugin.native.js 5.4kB 398.06kB 1.38%
view changes for bundle: plugins/reference-assets/core

Assets Changed:

Asset Name Size Change Total Size Change (%)
ReferenceAssetsPlugin.native.js 18.32kB 467.12kB 4.08%
cjs/index.cjs 2.33kB 13.52kB 20.81% ⚠️
index.legacy-esm.js 2.31kB 11.67kB 24.73% ⚠️
index.mjs 2.31kB 11.67kB 24.73% ⚠️
view changes for bundle: plugins/async-node/core

Assets Changed:

Asset Name Size Change Total Size Change (%)
AsyncNodePlugin.native.js 14.66kB 440.33kB 3.44%
cjs/index.cjs 5.03kB 22.43kB 28.88% ⚠️
index.legacy-esm.js 4.93kB 20.16kB 32.42% ⚠️
index.mjs 4.93kB 20.16kB 32.42% ⚠️
view changes for bundle: plugins/common-expressions/core

Assets Changed:

Asset Name Size Change Total Size Change (%)
CommonExpressionsPlugin.native.js 5.4kB 405.88kB 1.35%
view changes for bundle: plugins/common-types/core

Assets Changed:

Asset Name Size Change Total Size Change (%)
CommonTypesPlugin.native.js 5.4kB 430.28kB 1.27%

@tmarmer
Copy link
Contributor Author

tmarmer commented Feb 13, 2026

/canary

intuit-svc added a commit to player-ui/player-ui.github.io that referenced this pull request Feb 13, 2026
@intuit-svc
Copy link
Contributor

Build Preview

Your PR was deployed by CircleCI #31569 on Fri, 13 Feb 2026 14:55:49 GMT with this version:

0.15.1--canary.802.31569

📖 Docs (View site)

@tmarmer tmarmer requested a review from a team as a code owner February 24, 2026 21:45
@tmarmer tmarmer marked this pull request as draft March 10, 2026 18:57
Comment on lines +45 to +60
/** Returns a function to be used as the `replacer` for JSON.stringify that tracks and ignores circular references. */
const makeJsonStringifyReplacer = (): ReplacerFunction => {
const cache = new Set();
return (_: string, value: any) => {
if (typeof value === "object" && value !== null) {
if (cache.has(value)) {
// Circular reference found, discard key
return "[CIRCULAR]";
}
// Store value in our collection
cache.add(value);
}
return value;
};
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to do this? Shouldn't internal Player state be serializable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue here stems from the ResolverError adding in the failed node in its metadata. We need a better solution but this was put in place to allow the metadata to get logged without throwing errors since the node itself has circular references.

If we don't want the node logged or set in the dataController as part of captureError we either need to not include the metadata in logs/data or we need another mechanism to identify resolver nodes with to avoid sending the whole object to keep its reference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is so that the error includes the originating AST node, gotcha. I think a reference should be able to be stored fine in the Data Controller without serialization right? For logging can we just reference the asset ID?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the error controller was aware of the metadata being logged we could, but because any error with any metadata could pass through it, we can't identify when we're getting something like a Node or a simpler object. That and sometimes the originating Node being logged might be an async node or something else that generated the asset.

Comment on lines +204 to +211
export const ResolverStages = {
ResolveOptions: "resolve-options",
SkipResolve: "skip-resolve",
BeforeResolve: "before-resolve",
Resolve: "resolve",
AfterResolve: "after-resolve",
AfterNodeUpdate: "after-node-update",
} as const;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way we can automate this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't find a way to automatically form this object, but simplified the types here to use an enum. If you have any ideas here I'm open to try something else

tmarmer and others added 4 commits March 23, 2026 11:24
Co-authored-by: Ketan Reddy <KetanReddy@users.noreply.github.com>
Co-authored-by: Ketan Reddy <KetanReddy@users.noreply.github.com>
…etadata.test.ts

Co-authored-by: Ketan Reddy <KetanReddy@users.noreply.github.com>
Copy link
Contributor

@cehan-Chloe cehan-Chloe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@tmarmer tmarmer changed the title [WIP]: render-time error recovery add render-time and async node error recovery pattern Mar 25, 2026
@tmarmer tmarmer marked this pull request as ready for review March 25, 2026 20:34
@tmarmer tmarmer requested a review from a team as a code owner March 25, 2026 20:34
Copy link
Contributor

@KVSRoyal KVSRoyal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS stuff lgtm---can you just double-check that everything public actually needs to be?

Comment on lines +43 to +50
override val type: String
get() = ErrorTypes.RENDER
override val severity: ErrorSeverity?
get() = ErrorSeverity.ERROR
override val metadata: Map<String, Any?>?
get() = mapOf(
"assetId" to rootAsset.asset.id,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are static, just assign them:

Suggested change
override val type: String
get() = ErrorTypes.RENDER
override val severity: ErrorSeverity?
get() = ErrorSeverity.ERROR
override val metadata: Map<String, Any?>?
get() = mapOf(
"assetId" to rootAsset.asset.id,
)
override val type: String = ErrorTypes.RENDER
override val severity: ErrorSeverity? = ErrorSeverity.ERROR
override val metadata: Map<String, Any?> = mapOf(
"assetId" to rootAsset.asset.id,
)

cause: Throwable? = node.getSerializable("innerErrors", ListSerializer(ThrowableSerializer()))?.first(),
override val type: String = "",
override val severity: ErrorSeverity?,
override val metadata: Map<String, Any?>?,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these belong on the base JSErrorException type, as that is supposed to mirror the base Error TS class. We probably want a subclass that matches the PlayerErrorMetadata one

* Represents a Player error with metadata
*/
@Serializable(with = PlayerErrorInfo.Serializer::class)
public class PlayerErrorInfo internal constructor(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this whole class just be a PlayerException (with metadata) instance instead of a NodeWrapper? It could then just use the ThrowableSerializer instead of a whole new construct

coroutineExceptionHandler =
config.coroutineExceptionHandler ?: CoroutineExceptionHandler { _, throwable ->
if (state !is ReleasedState) {
logger.error("[HeadlessPlayer]: Error has been found")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidate the two logs now — just pull the other one up if you think we should log it every time regardless of whether we can forward it to captureError.

Comment on lines +23 to +30
override val type: String
get() = "TestError"
override val severity: ErrorSeverity?
get() = ErrorSeverity.ERROR
override val metadata: Map<String, Any?>?
get() = mapOf(
"testProperty" to "testValue",
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assign instead of getters

public fun setupPlayer(
plugins: List<Plugin> = this.plugins,
runtime: Runtime<*> = this.runtime,
includeThisInPlugins: Boolean = true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than an extra param, should we just do this automatically if this isn't contained in plugins?

val refPlugin = ReferenceAssetsPlugin()
refPlugin.apply(runtime)
if (player is HeadlessPlayer) {
val invokable = (player as HeadlessPlayer).node.getInvokable<Unit>("registerPlugin")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just implement registerPlugin properly than hack it into a test.

}

val errorMessage = assertThrows<Exception> {
val refPlugin = ReferenceAssetsPlugin()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this already applied on the Player instance?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants