Repository navigation
Support async asset error recovery and preserve render failures - #12290
alec-watts wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
ℹ️ No blocking issues — the async recovery contract reads coherently and is well covered by the new real-browser tests. Two inline notes on the shared plumbing, plus two docs-pipeline follow-ups.
Reviewed changes
Read the full diff (32 files) end to end, and compared the base origin/main implementations of on-error.ts, use-delay-render.tsx, and cancel-render.ts.
- Shared async recovery contract — new
MediaErrorHandler/resolveMediaError/handleImageError/useErrorRecoveryCommitinpackages/core, wired into<Img>,<CanvasImage>,<AnimatedImage>,<Gif>, and@remotion/media<Video>/<Audio>. - Original-error preservation —
cancel-render.tsnow storesremotion_cancelledErrorObject, andwait-for-ready.tsrejects with the originalError(cause/custom fields intact), prioritising cancellation over readiness. - Scoped cancellation fix — audio/video rendering now cancel through the active
useDelayRender()scope instead of importing the globalwindow-boundcancelRender. - Preview initialization failures — routed through the same async path; a
failnow surfaces viaterminalErrorrather than a rejected promise, andonErroris invoked once. - Docs + tests — new
asset-error-handlingpage plus per-component notes, real-browser recovery tests, and a media preview rejection test.
ℹ️ New docs page is missing from the generated articles catalog
packages/docs/src/data/articles.ts is a committed, generated index of every docs page (written by render-cards.ts, which walks docs/**/*.mdx) and drives the per-page social cards. The new asset-error-handling.mdx page has no entry, so the social-card catalog desyncs until it is regenerated.
Technical details
# Missing generated entry for the new docs page
## Affected sites
- `packages/docs/docs/asset-error-handling.mdx` — new page, not present in the catalog
- `packages/docs/src/data/articles.ts` — no `compId: 'articles-docs-asset-error-handling'` / `slug: 'asset-error-handling'` entry
## Required outcome
The new page appears in `articles.ts` with the same field set/ordering the generator produces (`id`, `title`, `relativePath`, `compId`, `crumb`, `noAi`, `slug`).
## Suggested approach
Run `bun render-cards` from `packages/docs` (it rewrites `articles.ts` and renders only missing cards), or add the entry by hand matching the generator's field ordering.ℹ️ New async behavior carries no <AvailableFrom> marker
This PR extends public callbacks (onImageError, onError on <CanvasImage>/<AnimatedImage>/<Gif>/<Video>/<Audio>) and exports a new public type MediaErrorHandler, but the docs don't say which version supports it. The repo convention is to annotate new public API with <AvailableFrom v="..." /> (the update-version skill exists for exactly this).
Technical details
# Missing version annotation for new public API
## Affected sites
- `packages/docs/docs/asset-error-handling.mdx` — new page, no version
- `packages/docs/docs/img.mdx:113`, `canvasimage.mdx:110`, `animatedimage.mdx:31`, `gif/gif.mdx:38`, `media/video.mdx:203`, `media/audio.mdx:155` — added "Supports asynchronous asset recovery" lines, no version
- `packages/core/src/index.ts:263` — new `MediaErrorHandler` export
## Required outcome
Mark the new behavior / type with the next patch version (main + 0.0.1) so users can tell which release added async recovery.deepseek-v4.1-flash | 𝕏
…t-error-recovery # Conflicts: # packages/core/src/animated-image/AnimatedImage.tsx # packages/gif/src/props.ts
JonnyBurger
left a comment
There was a problem hiding this comment.
Can you think about these points if they are tenable or could lead to a serious breakage? There are some behavior changes to review:
Breaking changes
1. TypeScript: some existing callbacks stop compiling. The old type (error: Error) => void let a callback return anything. The new 'handled' | 'fail' | void | Promise<…> union doesn't get that exception, so these used to compile and now fail (verified with tsc):
<CanvasImage onError={(e) => Sentry.captureException(e)} /> // returns a string
<Img onImageError={(e) => errors.push(e)} /> // returns a number
<AnimatedImage onError={(e) => (ref.current = e)} /> // returns ErrorThis affects Img.onImageError, CanvasImage, AnimatedImage and Gif.
2. MediaErrorAction gained 'handled'. It's exported from @remotion/media, so exhaustive switch statements with a never check stop compiling. Minor.
Bug: callbacks returning any other value now hang the render
This is the runtime side of point 1, for plain JS users, casts, or anyone who silences the type error. In resolveMediaError, any value other than undefined counts as an explicit decision:
action: action ?? defaultAction,
isDefaultAction: action === undefined,handleImageError only acts on 'fail', or on 'handled' when it was the default. Any other value, like Sentry's event ID string or a fetch() Response, does nothing, so the render pause is never released.
- Before:
onError={(e) => Sentry.captureException(e)}onCanvasImageorAnimatedImagelogged the error and the render continued. - After: the render waits until the
delayRendertimeout. The same goes forGifduration discovery, and foronError={(e) => fetch('/log', …)}.
Suggested fix: treat any value that isn't a known action as the default (isDefaultAction: true). The public type could then also accept any return value, which avoids the compile break.
Smaller problems
- Logging callbacks now pause the render. Async
CanvasImage/AnimatedImage/Gifcallbacks that send telemetry used to run in the background. Now the render waits for them, and a slow endpoint can cause adelayRendertimeout. This is intended, but it's a behavior change worth documenting. - Worse error message in browser rendering. In
callOnErrorAndResolve, an explicit'fail'returns before the browser-rendering branch. A Video/Audio handler that returns'fail'gets the bareUnknown container format X.instead ofCannot render video "X": Unknown container format. See supported formats: …. - Video/Audio
onErrornow runs during browser rendering, where it was skipped for these failures before. Handlers with side effects (toasts, analytics) will start firing there. - Fewer preview log lines.
video-for-preview.tsxno longer logsCannot decode X, falling back to <OffthreadVideo>,No video or audio tracks found…, falling back, or[VideoForPreview] Failed to initialize MediaPlayer, so fallbacks now happen silently. - A throwing
onImageErrornow cancels the render. Before, it was an uncaught error from an event handler. Probably better, but still a behavior change. <Gif onError>in preview. If the handler fails, the raw error now goes to the error boundary, skipping the CORS-specific error message.
|
Addressed these in 903e869, after merging current main:
Validation: 724 core tests; 64 browser recovery cases (two additional GIF preview cases skipped only in WebKit because the worker does not deliver the parse error in this harness); 7 media regression cases; all affected builds/typechecks. Removing the fixes reproduces the type break, all four incidental-return rendering failures, lost GIF source context, and lost explicit-failure context. The PR description has the full validation and per-file rationale. |
# Conflicts: # packages/core/src/Img.tsx # packages/media/src/audio/audio-for-preview.tsx # packages/media/src/audio/audio-for-rendering.tsx # packages/media/src/video/video-for-preview.tsx # packages/media/src/video/video-for-rendering.tsx
# Conflicts: # packages/docs/sidebars.ts

Asset recovery callbacks can now await URL refreshes or application replacements consistently across Img, CanvasImage, AnimatedImage, Gif, Video, and Audio. Returning
handledkeeps rendering paused until the replacement or removal commits; throwing/rejecting cancels with the application's Error, including cause and custom fields in browser-local rendering. Results from obsolete sources are ignored.The review fixes preserve existing callbacks returning incidental strings, numbers, Errors, or Responses: only recognized action strings select a policy; everything else uses the component's legacy default. The exported
MediaErrorActionremainsfallback | fail, so existing exhaustive switches compile. The newhandleddecision is accepted byMediaOnErrorwithout widening that old alias.Explicit Video/Audio
failretains browser-rendering context and the asset error as cause. Handler replacement errors retain identity. Preview fallback logging is restored. GIF original failures retain source/CORS context, while handler replacement errors remain unchanged.The guide documents the intended behavior changes: returned telemetry promises are awaited; Video/Audio handlers run during browser rendering; throwing image handlers cancel the render. Existing native Img void callbacks still wait for load or replacement.
Validation
903e8697df; this verification predates the main merge below.6f214a19aa.Why each file changes
bun.lockpackages/core/src/Img.tsxpackages/core/src/animated-image/AnimatedImage.tsxpackages/core/src/animated-image/props.tspackages/core/src/cancel-render.tspackages/core/src/canvas-image/CanvasImage.tsxpackages/core/src/canvas-image/props.tspackages/core/src/handle-image-error.tspackages/core/src/index.tspackages/core/src/internals.tspackages/core/src/media-error-handler.tspackages/docs/docs/animatedimage.mdxpackages/docs/docs/asset-error-handling.mdxpackages/docs/docs/canvasimage.mdxpackages/docs/docs/gif/gif.mdxpackages/docs/docs/img.mdxpackages/docs/docs/media/audio.mdxpackages/docs/docs/media/video.mdxpackages/docs/sidebars.tspackages/docs/src/data/articles.tspackages/gif/src/Gif.tsxpackages/gif/src/GifForDevelopment.tsxpackages/gif/src/GifForRendering.tsxpackages/gif/src/props.tspackages/media/src/audio/audio-for-preview.tsxpackages/media/src/audio/audio-for-rendering.tsxpackages/media/src/on-error.tspackages/media/src/test/video-onerror-stable.test.tsxpackages/media/src/video/video-for-preview.tsxpackages/media/src/video/video-for-rendering.tsxpackages/web-renderer/package.jsonpackages/web-renderer/src/test/asset-error-recovery.test.tsxpackages/web-renderer/src/wait-for-ready.tsCurrent main integration
Merged upstream
bbe11edd85in5c0339ef89. 64 recovery browser tests passed, 2 documented WebKit GIF-preview skips. Affected-package builds/typechecks passed; Upstream CI passed on5c0339ef89; GitHub reports no merge conflicts.Main refresh — 2026-10-23
Merged upstream
2bd172f29ainto this branch asf2ca672618. The merge required no manual conflict resolutions. Diff cleanup and whitespace checks passed; retained file changes are scoped to this proposal. Fresh upstream CI passed on this head, including Required CI. Earlier local validation above applies to the commits identified there.Local verification on this refreshed head: core TypeScript declaration build passed; all 724 core tests passed.