Shots in this scene
Skips, Errors and Fallbacks
In one line
A transition can be skipped by you, replaced by a newer one, rejected by a throwing update, abandoned for a duplicate name or a slow update, or never offered at all. Each case settles the three promises differently, and the DOM change must happen in all of them.
Think of it like this
A timetable has a fast train with a dining car and a slow one that stops everywhere. If the fast train is cancelled, passengers still reach the city: only the dining car is lost. The transition is the dining car; your DOM change is the journey and must always happen.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- skipTransition()
- Ends the animation at once and shows the new state. The update still runs.
- AbortError
- What
readyrejects with when a transition is skipped or replaced. - unhandled rejection
- A rejected promise nobody handles:
Uncaught (in promise). - scoped transition
- A transition started on an element, limited to that element's subtree.
Step by step
01Skip it, replace it
The first example skips a transition at once, then starts two in a row. Three updates ran in all (skipped, replaced, second), so the box toggled three times and ends wide: no update is lost.
02One guard for everything
The second example puts the API check, reduced motion and ready.catch in one helper. With the timeline's Reduced motion on, the log says transition: none, update ran directly and no tracks appear.
03Scope it
The third example starts the transition on a div: the tracks say div#scope::view-transition-… instead of html::view-transition-… (Pause, then Run again, then Step), and the outside button is still under the pointer during the animation.
04Know the slow case
An update that takes 5 seconds freezes the page, then ready rejects at 4 s (the code block below, measured in a plain page).
Try it yourself
- 1
See the unhandled error
In the skip example add
document.startViewTransition(() => {}); document.startViewTransition(() => {});after the closing})();. The console prints two lines (measured), eachUncaught (in promise) AbortError: Transition was skipped. New ViewTransition started: the first call was skipped by the second, and the second by the example's own calls that start after it. Add.ready.catch(() => {})to both calls and they are gone (measured: 0 lines).
Code & diagrams
Each line settles all three promises. The last line proves both updates ran.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
skipTransition() at once | updateCallbackDone fulfilled | ready rejected AbortError: Transition was skipped. skipTransition() called | finished fulfilled
first, replaced | updateCallbackDone fulfilled | ready rejected AbortError: Transition was skipped. New ViewTransition started | finished fulfilled
second | updateCallbackDone fulfilled | ready fulfilled | finished fulfilled
box is wide: both updates ranSwitch the timeline's Reduced motion on: the log changes and no transition tracks appear. The change itself always happens.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
reduced motion: false, transition: startedThe transition is limited to the dashed box. The first run plays at load, before you can pause: press Pause, then **Run again**, then Step. The tracks name `div#scope`, not `html`.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
element.startViewTransition is a function
under the outside button: BUTTON
pseudo-elements hang off: scopeconst slow = document.startViewTransition(() => new Promise((resolve) => setTimeout(resolve, 5000)));
// ready rejected at 4 s:
// updateCallbackDone fulfilled
// ready rejected TimeoutError: Transition was aborted because of timeout in DOM update. DOM update timed out
// finished fulfilledBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use the document's transition for a small widget
In the scoped example change document.getElementById("scope").startViewTransition( to document.startViewTransition( in the async function at load (the first one).
Myth vs fact
Myth
If the transition is skipped the DOM change is skipped too.
Fact
Never. skipTransition() skips only the animation. The update callback runs in every case.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A UI that re-renders per keystroke or slider tick would snapshot each time, and a running transition blocks pointer events for its whole animation. Use transitions for clear, rare changes (a tab, a sort, a page), keep them near 200 ms on frequent controls, and give typing none.
Remember this
- 1
The problem this solves: demos show the happy path. In production the visitor clicks twice, the update throws, two cards share a name, the browser is old, or the visitor asked for less motion. If your code assumes the animation always plays, the change can be lost or the console fills with unhandled errors.
- 2
Recall from Topic 11.1:
updateCallbackDone,readyandfinishedsettle in that order for a transition that plays; this topic shows what each does when it does not. - 3
Your update runs in every case: skipped, replaced, timed out or abandoned for a duplicate name (all measured). It does not run only when the browser lacks
startViewTransition, which is why Topic 11.1 guards the call. - 4
What each promise does. Skipped (
skipTransition()) or replaced (a newer call):readyrejects withAbortError(Transition was skipped. skipTransition() calledor... New ViewTransition started), the other two fulfill. Duplicate names:readyrejects withInvalidStateError. Update slower than about 4 s:readyrejects withTimeoutError. Update throws or rejects: all three reject with that error. Soreadyis the one to catch, andfinishedmeans "the new state is live". - 5
**An uncaught
readyis an error in the console:**Uncaught (in promise) AbortError: Transition was skipped. New ViewTransition started(measured). The page works, but the noise hides real errors: putt.ready.catch(() => {})in the one helper all calls go through. - 6
Reduced motion belongs in the same guard:
if (!document.startViewTransition || matchMedia('(prefers-reduced-motion: reduce)').matches) { update(); return; }. In CSS you can keep a quick fade instead (the Lumen build does). Either way the state change happens. - 7
Scoped transitions.
element.startViewTransition(...)limits a transition to one subtree (Chromium 153 has it; MDN marks it experimental, not Baseline). Measured: the pseudo-elements hang off that element, with their ownroot, and the rest of the page stays clickable.document.activeViewTransitionis the running transition ornull(measured), sodocument.activeViewTransition?.skipTransition()skips whatever runs.
Explain it without notes
Which promise rejects when a transition is skipped, with which error, and does the DOM still update? Why wrap startViewTransition in one helper?
When would you use element.startViewTransition instead of the document's?
Practice
Write withTransition(update, types) that handles the missing API, reduced motion and skipped transitions, and returns the transition or null.
A search box re-renders results per keystroke. Apply a transition to a tab change only.
Done when you can
You can say how each failure settles the three promises
You can write the one guard: API, reduced motion and
ready.catchYou can say when a scoped transition helps