Skip to main content
Every fallible operation in BlindCast returns Result<T, E> — a discriminated union — instead of throwing exceptions. This makes error handling explicit, type-safe, and impossible to accidentally ignore.
The CLI is the exception — it uses Unix exit codes instead of Result types. See CLI Error Codes.

The type

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }
  • When ok is true, value holds the success payload.
  • When ok is false, error holds the typed error.
TypeScript narrows the type automatically based on the ok check.

Why not throw?

BlindCast uses Result in the player, uploader, and internal packages for these reasons:
  1. Errors are part of the API contract. A function’s return type documents what can go wrong — callers can’t miss it.
  2. No unexpected try/catch. You won’t accidentally swallow a crypto error in a generic catch block.
  3. TypeScript narrowing works naturally. After if (!result.ok), TypeScript knows result.error is typed and result.value is not accessible.
  4. Compatible with async/await. Result composes cleanly with Promise<Result<T, E>> — no need to mix try/catch with await.

Pattern 1: Early return

The simplest pattern — bail out immediately on failure:
import { createPlayer } from "@blindcast/player"

const result = createPlayer(videoEl, {
  keyServerUrl: "https://keys.example.com/keys",
})

if (!result.ok) {
  console.error("Player creation failed:", result.error.code)
  return
}

const player = result.value // TypeScript knows this is BlindcastPlayer here
player.load(manifestUrl)

Pattern 2: Switch on error code

When different errors need different handling:
import { createPlayer } from "@blindcast/player"
import type { PlayerErrorCode } from "@blindcast/player"

const result = createPlayer(videoEl, { keyServerUrl })

if (!result.ok) {
  switch (result.error.code as PlayerErrorCode) {
    case "UNSUPPORTED_BROWSER":
      showFallbackMessage("Your browser does not support encrypted video")
      break
    case "INVALID_VIDEO_ELEMENT":
      console.error("Bug: video element not found in DOM")
      break
    case "KEY_FETCH_FAILED":
      showRetryButton()
      break
    default:
      console.error("Unexpected error:", result.error.code, result.error.message)
  }
  return
}

const player = result.value

Pattern 3: Unwrap helper

If you prefer to throw in contexts where any error is fatal (e.g., scripts, tests):
function unwrap<T, E extends { code: string; message: string }>(
  result: { ok: true; value: T } | { ok: false; error: E },
  context?: string,
): T {
  if (!result.ok) {
    throw new Error(`${context ? context + ": " : ""}${result.error.code}${result.error.message}`)
  }
  return result.value
}

// Usage
const player = unwrap(createPlayer(videoEl, opts), "createPlayer")

Where you’ll encounter Result types

DeliverableUses Result?Error type
PlayerYesPlayerError with PlayerErrorCode
UploaderYesUploaderError with UploaderErrorCode
CLINo — uses exit codesSee CLI exit codes
Key Server DockerNo — uses HTTP status codes400, 401, 403, 500
Crypto (internal)YesCryptoError with CryptoErrorCode

TypeScript tips

Never access .value without checking .ok first

// TypeScript will error — value might not exist
const key = result.value

// Narrow first
if (result.ok) {
  const key = result.value // CryptoKey
}

The error type varies per deliverable

Each library defines its own error type with a specific code union:
import type { PlayerError, PlayerErrorCode } from "@blindcast/player"
import type { UploaderError, UploaderErrorCode } from "@blindcast/uploader"
All error types share the shape { code: ErrorCode; message: string }. PlayerError additionally has an optional cause?: unknown field for wrapping underlying hls.js errors.