Error Handling
Video playback relies on a number of coordinated subsystems — manifest retrieval, DRM license acquisition, audio and video decoding, network I/O, and rendering. Failures in any of these subsystems may affect the user experience and must be surfaced to the application in a consistent and actionable form.
This guide describes how errors are modelled in PRESTOplay for React Native, how they behave across the supported platforms, and how to handle them in an application.
Table of contents
- How errors are modelled
- Severity: recoverable vs. fatal
- Listening for errors
- Network errors
- Example: handling errors on playback opening
- Example: recovering from an expired CDN token (HTTP 403)
How errors are modelled
Every error reported by the player is represented by a PrestoPlayError instance. This class provides a normalized, cross-platform representation of the underlying native error (Android, iOS/tvOS, or Web). Its most relevant fields are:
| Field | Description |
|---|---|
code | A stable numeric ErrorCode identifying the kind of error (e.g. ManifestLoadingFailed, MediaDownloadingFailed, DrmError). |
severity | An ErrorSeverity value, either Recoverable or Fatal. |
category | An ErrorCategory value derived from the code (Network, Media, Manifest, Drm, Player, etc.), intended for routing errors to the appropriate handler. |
message / causeMessage | Human-readable descriptions provided by the native SDK. |
network | A NetworkErrorMetadata object containing httpStatusCode, url, httpResponseBody, and a cause (NetworkErrorCause). |
nativeError | The raw, platform-specific error object, available when additional inspection beyond the normalized fields is required. |
Severity: recoverable vs. fatal
The SDK classifies every error with one of two severities:
Recoverable— informative. Playback has not been interrupted. The player typically continues from its buffer while retrying the failing operation internally. Recoverable errors provide the application with an opportunity to act before the situation escalates — for example, to refresh an authentication token before the buffer is exhausted.Fatal— terminal. Playback cannot start or cannot continue. The player stops, and the application should present an appropriate message, fall back to an alternative stream, or retry with corrected configuration.
A single underlying problem may manifest first as one or more recoverable warnings and, if left unaddressed, escalate into a fatal error. This pattern is illustrated in the CDN token recovery example below.
Listening for errors
Two complementary mechanisms are available for observing errors.
1. The usePlayerFatalError hook is appropriate for rendering a fallback user interface (banner, modal, error screen) when playback has definitively failed. It filters on fatal severity and clears automatically when Player.open or Player.stop is invoked.
import { usePlayerFatalError } from "@castlabs/react-native-prestoplay";
function FatalErrorBanner() {
const fatalError = usePlayerFatalError();
if (!fatalError) return null;
return (
<View>
<Text>Playback failed (code {fatalError.code})</Text>
<Text>{fatalError.message}</Text>
</View>
);
}
2. The Error event is appropriate when the application needs visibility into every error — including recoverable ones — in order to apply custom logic (retry, refresh a token, report to analytics). The same event is also the building block for implementing a custom hook tailored to the application's needs — for example, one that exposes only errors of a given category, keeps a rolling history for diagnostics, or couples error state with retry logic — when usePlayerFatalError does not fit the use case.
import {
EventType,
ErrorDetails,
ErrorSeverity,
usePlayer,
} from "@castlabs/react-native-prestoplay";
function useErrorLogger() {
const player = usePlayer();
useEffect(() => {
const onError = (event) => {
const { error } = event.details as ErrorDetails;
console.log(
`[Player] ${ErrorSeverity[error.severity]} error ${error.code}:`,
error.message,
error.network,
);
};
player.addEventListener(EventType.Error, onError);
return () => player.removeEventListener(EventType.Error, onError);
}, [player]);
}
Building a custom hook
The useErrorLogger example above is a minimal illustration of a more general pattern. When the behaviour offered by usePlayerFatalError does not match a particular requirement, applications are encouraged to encapsulate their own error-observation logic in a dedicated hook. Typical examples include:
- A hook that exposes only errors of a specific ErrorCategory (e.g.
useNetworkError,useDrmError). - A hook that maintains a rolling history of the last N errors, intended for diagnostics screens or telemetry reports.
- A hook that combines error observation with retry policy (backoff, maximum attempts, fallback to an alternative stream).
- A hook that filters on a specific condition, such as the recoverable HTTP
403handler shown in the CDN token recovery example below.
The recommended structure is the one used throughout this guide: subscribe to EventType.Error inside a useEffect, update component state (or perform side effects) based on the error's severity, code, category, or network fields, and unsubscribe in the cleanup function to respect the React component lifecycle.
Network errors
Network errors are the most common category of errors in streaming applications. They cover every failure involving an HTTP request performed by the player — manifest retrieval, media segment download, DRM license acquisition, and connectivity changes. All errors in this category share the code range 1000–1999 and resolve to ErrorCategory.Network.
The network metadata
For errors in this category, the network field of PrestoPlayError is populated with additional diagnostic information, captured in the NetworkErrorMetadata interface:
| Field | Description |
|---|---|
url | The URL of the failed request, when available. |
httpStatusCode | The HTTP status code returned by the server, when the server could be reached. -1 indicates no response received. |
httpResponseBody | The body of the response, when available. Useful for surfacing backend error messages. |
cause | A NetworkErrorCause value describing the nature of the failure. |
The cause field takes one of the following values:
| Value | Meaning |
|---|---|
HttpError | The server was reached but returned a non-success HTTP status code (e.g. 400, 401, 403, 404, 5xx). |
ServerUnreachable | The request could not reach the server (e.g. DNS failure, connection refused, TLS failure, request cancelled). |
NoNetworkConnection | The device has no active network connection at the time the request was issued. |
Fatal vs. recoverable network errors
Network errors follow the general severity semantics described earlier in the guide. In practice:
- Errors raised during opening (before the player reaches
Ready) are almost always fatal, since playback cannot start without a manifest and decoders. Fatal errors during opening are normalized across platforms toManifestLoadingFailed(1002), regardless of whether the underlying failure was a manifest request, an initialization segment, or a license fetch. - Errors raised during playback are typically recoverable on the first occurrences, because the player continues from its buffer while retrying. They escalate to fatal once the retry policy is exhausted or the buffer drains. This is the pattern that makes mid-playback recovery (e.g. token refresh) possible.
ConnectivityLost(1003) andConnectivityGained(1004) are always informational and never interrupt playback on their own. They can be used to drive network-status indicators in the user interface.
Reading the HTTP status
Filtering on the HTTP status code is a common requirement. Typical dispatch logic looks as follows:
import {
EventType,
ErrorDetails,
ErrorCategory,
NetworkErrorCause,
usePlayer,
} from "@castlabs/react-native-prestoplay";
function useNetworkErrorHandler() {
const player = usePlayer();
useEffect(() => {
const onError = (event: { details: ErrorDetails }) => {
const { error } = event.details;
if (error.category !== ErrorCategory.Network) return;
const { httpStatusCode, cause, url } = error.network;
if (cause === NetworkErrorCause.NoNetworkConnection) {
// Device is offline. Show a connectivity banner.
return;
}
if (cause === NetworkErrorCause.ServerUnreachable) {
// Request never reached the server (DNS, TLS, timeout, cancellation).
return;
}
switch (httpStatusCode) {
case 401:
case 403:
// Authentication or authorization problem.
// See the CDN token recovery example below.
break;
case 404:
// Resource not found. Typically a configuration or packaging problem.
break;
case 429:
// Rate limited. Back off before retrying.
break;
default:
if (httpStatusCode && httpStatusCode >= 500) {
// Server error. Retry with backoff or fall back to another origin.
}
}
};
player.addEventListener(EventType.Error, onError);
return () => player.removeEventListener(EventType.Error, onError);
}, [player]);
}
Platform notes
- Safari does not surface HTTP
403responses to the player. Applications that depend on observing a 403 mid-playback should treat Safari as a best-effort target. DnsLookupFailed(1006) andTimeoutOccurred(1007) are currently emitted only on Android.
Example: handling errors on playback opening
Between the invocation of Player.open and the player reaching the Ready state, the SDK retrieves the manifest, negotiates DRM, and prepares the decoders. A failure during this phase — an unreachable URL, an invalid or expired license, a 500 Internal Server Error — is reported as a fatal error, since playback has not been established.
The recommended approach for handling these failures is to drive the user interface from the usePlayerFatalError hook. The component below opens a stream and presents a retry control when opening fails.
import React, { useCallback } from "react";
import { View, Text, Button, Alert } from "react-native";
import {
ContentType,
ErrorCategory,
PlayerProvider,
PlayerView,
usePlayer,
usePlayerFatalError,
} from "@castlabs/react-native-prestoplay";
function PlaybackSurface({ streamUrl }: { streamUrl: string }) {
const player = usePlayer();
const fatalError = usePlayerFatalError();
const retry = useCallback(() => {
player.open({
autoPlay: true,
source: { url: streamUrl, type: ContentType.Hls },
});
}, [player, streamUrl]);
if (fatalError) {
const title =
fatalError.category === ErrorCategory.Network
? "Network problem"
: fatalError.category === ErrorCategory.Drm
? "Playback not authorized"
: "Playback failed";
return (
<View>
<Text>{title}</Text>
<Text>
{fatalError.message} (code {fatalError.code})
</Text>
<Button title="Try again" onPress={retry} />
</View>
);
}
return <PlayerView />;
}
export function Screen({ streamUrl }: { streamUrl: string }) {
return (
<PlayerProvider
playerConfig={{
autoPlay: true,
source: { url: streamUrl, type: ContentType.Hls },
}}
>
<PlaybackSurface streamUrl={streamUrl} />
</PlayerProvider>
);
}
Points to note:
- The handler dispatches on
categoryrather than on individual error codes. A fatal error in theNetworkcategory is sufficient to inform the user that the server could not be reached; whether the specific code isManifestLoadingFailedorStreamLoadingFailedis rarely relevant at the presentation layer. - The retry control invokes
Player.openagain. This call transitions the player to theOpeningstate, which causesusePlayerFatalErrorto reset, so the interface automatically returns to rendering<PlayerView />. - For DRM-related failures (
ErrorCategory.Drm), a different recovery action is typically more appropriate — for example, prompting the user to re-authenticate. The category field enables this kind of dispatch.
Example: recovering from an expired CDN token (HTTP 403)
A common pattern in commercial streaming services is to authorize access to the CDN using a short-lived token embedded in the stream URL (frequently as a query parameter). The CDN validates this token on every segment request. When the token expires mid-playback, the CDN responds to subsequent requests with HTTP 403 Forbidden.
The sequence observed on the player side is the following:
- Playback is in progress, and the player holds a buffer of media segments ahead of the current position.
- The token expires. The CDN begins rejecting segment requests with
403. - The player emits an
Errorevent withseverity = Recoverable,code = MediaDownloadingFailed(1000),network.httpStatusCode = 403, andnetwork.cause = HttpError. - Playback continues from the existing buffer while the SDK retries the failing requests internally.
- If the condition is not addressed, the buffer eventually drains. At that point the same
MediaDownloadingFailederror is re-emitted withseverity = Fatal, and playback stops.
The recoverable error therefore represents the window during which the application can correct the underlying condition before the user experiences a stall. A typical recovery procedure is:
- Detect the recoverable
403. - Obtain a fresh authorization token from the backend.
- Re-open the stream with the refreshed URL, restoring the previous playback position to minimize the interruption.
import React, { useCallback, useEffect, useRef } from "react";
import {
EventType,
ErrorCode,
ErrorDetails,
ErrorSeverity,
ContentType,
NetworkErrorCause,
PlayerProvider,
PlayerView,
usePlayer,
} from "@castlabs/react-native-prestoplay";
async function fetchFreshStreamUrl(): Promise<string> {
const response = await fetch("https://my-backend.example.com/stream-url");
const { url } = await response.json();
return url;
}
function useCdnTokenRecovery() {
const player = usePlayer();
const refreshInFlight = useRef(false);
const recoverWithFreshToken = useCallback(async () => {
if (refreshInFlight.current) return;
refreshInFlight.current = true;
try {
const resumePosition = await player.getPosition();
const newUrl = await fetchFreshStreamUrl();
await player.open({
autoPlay: true,
source: { url: newUrl, type: ContentType.Hls },
startTime: resumePosition,
});
} catch (e) {
console.warn("Failed to refresh CDN token:", e);
} finally {
refreshInFlight.current = false;
}
}, [player]);
useEffect(() => {
const onError = (event: { details: ErrorDetails }) => {
const { error } = event.details;
const isRecoverable403 =
error.severity === ErrorSeverity.Recoverable &&
error.code === ErrorCode.MediaDownloadingFailed &&
error.network.cause === NetworkErrorCause.HttpError &&
error.network.httpStatusCode === 403;
if (isRecoverable403) {
recoverWithFreshToken();
}
};
player.addEventListener(EventType.Error, onError);
return () => player.removeEventListener(EventType.Error, onError);
}, [player, recoverWithFreshToken]);
}
function PlaybackSurface() {
useCdnTokenRecovery();
return <PlayerView />;
}
export function Screen({ initialUrl }: { initialUrl: string }) {
return (
<PlayerProvider
playerConfig={{
autoPlay: true,
source: { url: initialUrl, type: ContentType.Hls },
}}
>
<PlaybackSurface />
</PlayerProvider>
);
}
Design notes:
- Precise filtering. The handler acts only when all four conditions match: recoverable severity,
MediaDownloadingFailed, HTTP cause, and status403. Other errors are left to be handled by other components (the fatal-error UI, the logging hook, etc.). - Refresh de-duplication. The player may emit the same recoverable error multiple times while retrying. The
refreshInFlightreference prevents concurrent invocations of the token-refresh procedure. - Position preservation. Reading
player.getPosition()prior to re-opening allows the session to resume at the same point, making the transition largely transparent to the user. - Acting before buffer exhaustion. As long as the refresh completes before the buffer drains, the new stream is opened without any visible interruption.
- Handling refresh failure. If the refresh procedure itself fails, the next
403will escalate to a fatal error. This recipe should therefore be combined with theusePlayerFatalErrorpattern described earlier, so that an unsuccessful recovery still produces a coherent error screen.
Insights
- Errors cross the React Native bridge asynchronously. Errors propagate from the native SDK, through the corresponding native module, across the bridge, and into the JavaScript listener. They should be observed through events rather than inferred from the result of the call that triggered them.
- React component lifecycle must be respected. Event listeners registered with
addEventListenermust be removed withremoveEventListenerduringuseEffectcleanup, or the application should rely on the provided hooks (such as usePlayerFatalError) that manage subscriptions automatically.