jamiepine/voicebox · critical · Error

Navigator API is not available. This might be a Tauri config

Error message

Navigator API is not available. This might be a Tauri configuration issue.

What it means

Thrown at the very top of `startRecording()` in `useAudioRecording.ts` (line 34-39) when `typeof navigator === 'undefined'`. In any real browser/Tauri webview the global `navigator` always exists, so hitting this branch means the code is executing outside a client DOM environment — most often during SSR/prerender or in a JS runtime without a DOM.

Source

Thrown at app/src/lib/hooks/useAudioRecording.ts:38

  const streamRef = useRef<MediaStream | null>(null);
  const timerRef = useRef<number | null>(null);
  const startTimeRef = useRef<number | null>(null);
  const cancelledRef = useRef<boolean>(false);

  const startRecording = useCallback(async () => {
    try {
      setError(null);
      chunksRef.current = [];
      cancelledRef.current = false;
      setDuration(0);

      // Check if getUserMedia is available
      // In Tauri, navigator.mediaDevices might not be available immediately
      if (typeof navigator === 'undefined') {
        const errorMsg =
          'Navigator API is not available. This might be a Tauri configuration issue.';
        setError(errorMsg);
        throw new Error(errorMsg);
      }

      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
        // Try waiting a bit for Tauri webview to initialize
        await new Promise((resolve) => setTimeout(resolve, 100));

        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
          console.error('MediaDevices check:', {
            hasNavigator: typeof navigator !== 'undefined',
            hasMediaDevices: !!navigator?.mediaDevices,
            hasGetUserMedia: !!navigator?.mediaDevices?.getUserMedia,
            isTauri: platform.metadata.isTauri,
          });

          const errorMsg = platform.metadata.isTauri
            ? 'Microphone access is not available. Please ensure:\n1. The app has microphone permissions in System Settings (macOS: System Settings > Privacy & Security > Microphone)\n2. You restart the app after granting permissions\n3. You are using Tauri v2 with a webview that supports getUserMedia'
            : 'Microphone access is not available. Please ensure you are using a secure context (HTTPS or localhost) and that your browser has microphone permissions enabled.';
          setError(errorMsg);

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Only call `startRecording()` from a user gesture inside a client-mounted component (e.g. a button onClick), never during render or module load.
  2. If using SSR, ensure the recording UI is client-only (`'use client'`, or dynamic import with `ssr: false`).
  3. In tests, run under jsdom/happy-dom or mock `navigator`.
  4. Confirm the Tauri webview is a standard WKWebView/WebView2 — a custom host lacking `navigator` is a build/config defect.

Example fix

// before — runs during render/SSR
const { startRecording } = useAudioRecording();
startRecording();

// after — guard to client + user gesture
if (typeof navigator === 'undefined') return; // SSR/prerender
<button onClick={() => startRecording()}>Record</button>
Defensive patterns

Strategy: type-guard

Validate before calling

// Never call startRecording outside a client-mounted, user-gesture context
if (typeof navigator === 'undefined') {
  // SSR/prerender — bail silently instead of throwing
  return;
}

Type guard

function hasNavigator(): boolean {
  return typeof navigator !== 'undefined';
}

Try / catch

// In the component: only invoke from a gesture, after client mount
<button disabled={!hasNavigator()} onClick={() => startRecording()}>Record</button>

Prevention

When it happens

Trigger: The hook's `startRecording` runs during server-side rendering or static prerendering (Next.js/React SSR) where `navigator` is undefined; the code is unit-tested in Node without jsdom; the Tauri webview host failed to inject the standard web globals (misconfigured/custom host).

Common situations: A component using this hook is rendered on the server without a `'use client'`/`useEffect` guard; a test imports the hook and calls `startRecording` synchronously in a Node runner; an exotic embedded webview strips `navigator`.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/6e3b171181f17c63. Report an issue: GitHub.