Skip to content

Troubleshooting

Common issues and solutions when using Vizel.

Error Handling

Vizel surfaces errors through VizelError, a structured Error subclass with a stable code and an optional context object. Narrow with isVizelError(error) before reading the code.

ts
import { isVizelError } from '@vizel/react';

<Vizel
  onError={(error) => {
    if (!isVizelError(error)) return;
    if (error.code === 'UPLOAD_FAILED') {
      // user-facing toast
    } else {
      // ship to Sentry / Datadog
    }
  }}
/>

Error Code Reference

CodeCategoryWhen it firesSurfaceConsumer can recover by
INVALID_CONFIGConfigurationEditor construction with conflicting feature flags (e.g. comments without provider)Thrown and re-emitted via onErrorFix the option object and re-mount
INVALID_EXTENSIONConfigurationA custom extension fails internal validationThrownInspect the extension; align with Tiptap's contract
MISSING_CONTEXTConfigurationuseVizelContext / getVizelContext called outside a providerThrownWrap the consumer in <VizelProvider> or <Vizel>
INVALID_LOCALEConfigurationA VizelLocale is missing required keysThrownFill in the missing locale fields or use createVizelLocale()
SSR_NOT_SUPPORTEDConfigurationcreateVizelEditorInstance called on the serverThrownMove editor creation inside useEffect / onMounted / $effect
SSR_DOM_SHIM_MISSINGConfigurationA controller's mount() invoked without a DOM target on the serverThrownPass a DOM element or guard the controller behind the mount lifecycle
MISSING_OPTIONAL_DEPConfigurationA lazy-loaded peer (KaTeX, Mermaid, GraphViz) is not installedonErrorAdd the peer dependency listed in the message
INVALID_MARKDOWNInputMarkdown input rejected by the parseronErrorSanitize the input or pre-validate it
INVALID_JSON_CONTENTInputJSON content does not match the Tiptap schemaonErrorNormalize the content shape before passing it to setContent
INVALID_URLInputLink / embed handed an unparseable URLonErrorValidate the URL string before submitting
MARKDOWN_LOSSYInputThe active flavor cannot serialize a node losslesslyonError (severity "warning")Switch to a flavor that supports the node (e.g. vizelObsidianFlavor for wiki links), or accept the lossy output
UPLOAD_FAILEDRuntimeimage.onUpload rejected or threwonErrorRetry, show a toast, or restore the placeholder
EMBED_LOAD_FAILEDRuntimeEmbed metadata could not be fetchedonErrorShow a fallback link
CLIPBOARD_FAILEDRuntimeBrowser blocked a clipboard operationonErrorPrompt the user with a manual fallback
COLLAB_DISCONNECTEDCollaborationThe Yjs provider lost its connectiononErrorReconnect the provider; show offline indicator
COLLAB_SYNC_FAILEDCollaborationInitial sync did not completeonErrorVerify provider auth; retry with backoff
UNKNOWN_ERRORFallbackwrapAsVizelError called without a specific codeonErrorInspect error.cause for context

Configuration errors are loud by design

Configuration codes (INVALID_CONFIG, SSR_NOT_SUPPORTED, ...) are emitted to onError and rethrown so global handlers (Sentry, window.onunhandledrejection) observe them even when an observability onError handler is wired up. The previous v2.0.0 behavior — suppressing the rethrow when onError was set — silently blanked the editor for some consumers. If you only need telemetry, log the error and let it propagate; if you want to recover, catch the error at the parent boundary.

Editor Not Rendering

CSS Not Loaded

If the editor appears unstyled or broken, ensure you import the CSS:

typescript
// Import Vizel styles
import '@vizel/core/styles.css';

// For components like Bubble Menu (optional)
import '@vizel/core/styles/components.css';

// For mathematics support (optional)
import '@vizel/core/mathematics.css';

Editor Instance is Null

The editor hook/composable/rune returns null until initialization completes:

tsx
import { useVizelEditor, VizelEditor } from '@vizel/react';

function Editor() {
  const editor = useVizelEditor({});

  // Handle loading state
  if (!editor) {
    return <div>Loading...</div>;
  }

  return <VizelEditor editor={editor} />;
}
vue
<script setup lang="ts">
import { useVizelEditor, VizelEditor } from '@vizel/vue';

const editor = useVizelEditor({});
</script>

<template>
  <div v-if="!editor">Loading...</div>
  <VizelEditor v-else :editor="editor" />
</template>
svelte
<script lang="ts">
  import { createVizelEditor, VizelEditor } from '@vizel/svelte';

  const editor = createVizelEditor({});
</script>

{#if !editor.current}
  <div>Loading...</div>
{:else}
  <VizelEditor editor={editor.current} />
{/if}

Console Errors

Check your browser console for initialization errors. Common issues include:

  • Missing peer dependencies
  • Extension conflicts
  • Invalid initial content format

Image Upload Failures

Upload Handler Not Configured

By default, Vizel converts images to Base64. For production, you should configure a custom upload handler:

typescript
const editor = useVizelEditor({
  features: {
    content: {
      image: {
        onUpload: async (file) => {
          const formData = new FormData();
          formData.append('image', file);

          const response = await fetch('/api/upload', {
            method: 'POST',
            body: formData,
          });

          if (!response.ok) {
            throw new Error('Upload failed');
          }

          const { url } = await response.json();
          return url;
        },
      },
    },
  },
});

File Size Exceeded

You can configure maxFileSize and handle validation errors:

typescript
const editor = useVizelEditor({
  features: {
    content: {
      image: {
        maxFileSize: 5 * 1024 * 1024, // 5MB
        onValidationError: (error) => {
          if (error.type === 'file_too_large') {
            alert(`File too large: ${error.message}`);
          }
        },
      },
    },
  },
});

Invalid File Type

You can configure allowed MIME types:

typescript
const editor = useVizelEditor({
  features: {
    content: {
      image: {
        allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
        onValidationError: (error) => {
          if (error.type === 'invalid_type') {
            alert('Only JPEG, PNG, and WebP images are allowed');
          }
        },
      },
    },
  },
});

Network Errors

You should handle upload failures gracefully:

typescript
const editor = useVizelEditor({
  features: {
    content: {
      image: {
        onUpload: async (file) => {
          // Your upload logic
        },
        onUploadError: (error, file) => {
          console.error(`Failed to upload ${file.name}:`, error);
          // Show user-friendly error message
          alert('Upload failed. Please check your connection and try again.');
        },
      },
    },
  },
});

Performance Issues

Auto-Save Too Frequent

You can increase the debounce time for auto-save:

typescript
import { useVizelAutoSave, useVizelEditor } from '@vizel/react';

const editor = useVizelEditor();
useVizelAutoSave(editor, {
  debounceMs: 2000, // Wait 2 seconds after last change
  onSave: async (content) => {
    await saveToServer(content);
  },
});

Large Documents

For large documents, consider the following approaches:

  1. Disable unused features to reduce bundle size and processing overhead:
typescript
const editor = useVizelEditor({
  features: {
    content: {
      mathematics: false,  // Disable if not needed
      diagram: false,      // Disable if not needed
      embed: false,        // Disable if not needed
    },
  },
});
  1. Lazy load optional features:
typescript
// Only import mathematics CSS when needed
if (useMathematics) {
  import('@vizel/core/mathematics.css');
}
  1. Optimize code blocks with fewer languages:
typescript
import { createLowlight, common } from 'lowlight';

// Use only common languages instead of all
const lowlight = createLowlight(common);

const editor = useVizelEditor({
  features: {
    codeBlock: {
      lowlight,
    },
  },
});

Sluggish Typing

If typing feels slow:

  1. Check for expensive onUpdate handlers
  2. Debounce your state updates
  3. Avoid synchronous JSON serialization on every keystroke
typescript
// Bad: Expensive on every keystroke
onUpdate: ({ editor }) => {
  setContent(editor.getJSON()); // Triggers re-render
  saveToLocalStorage(editor.getJSON()); // Synchronous I/O
},

// Good: Debounced updates
const [content, setContent] = useState(null);
const debouncedSetContent = useMemo(
  () => debounce((json) => setContent(json), 300),
  []
);

onUpdate: ({ editor }) => {
  debouncedSetContent(editor.getJSON());
},

Common Error Messages

"Cannot read property 'commands' of null"

The editor instance is not yet initialized. Always check for null:

typescript
// Wrong
editor.commands.setContent(content);

// Correct
if (editor) {
  editor.commands.setContent(content);
}

// Or use optional chaining
editor?.commands.setContent(content);

"Maximum call stack size exceeded"

This error is usually caused by:

  1. Circular content updates: Do not update content inside onUpdate:
typescript
// Wrong: Causes infinite loop
onUpdate: ({ editor }) => {
  editor.commands.setContent(transformContent(editor.getJSON()));
},

// Correct: Use external trigger
const handleTransform = () => {
  if (editor) {
    editor.commands.setContent(transformContent(editor.getJSON()));
  }
};
  1. Recursive component rendering: Verify that you set proper dependency arrays in hooks.

"Extension not found"

Verify that you enabled the required features:

typescript
// Error when trying to use disabled feature
editor.commands.toggleMathInline(); // Fails if content.mathematics: false

// Solution: Enable the feature
const editor = useVizelEditor({
  features: {
    content: {
      mathematics: true,
    },
  },
});

"Adding different instances of a keyed plugin"

This error occurs when multiple instances of the same ProseMirror plugin are loaded. Common causes:

  1. Duplicate Tiptap packages in dependencies:
bash
# Check for duplicates
pnpm why @tiptap/core
npm ls @tiptap/core
  1. Incorrect bundling of @vizel/core:

Make sure your bundler treats @tiptap/* as external. For Vite:

typescript
// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    exclude: ['@tiptap/core', '@tiptap/pm'],
  },
});
  1. Multiple editor instances sharing the extensions array:

useVizelEditor / createVizelEditor build the always-on extension set internally; the extensions option is for additional Tiptap extensions a consumer wants to stack on top. Sharing the same array across editors works, but sharing a single ProseMirror plugin instance across editors does not. Create a fresh extension list per editor whenever any of the items is stateful:

typescript
// Correct: each editor builds its own extensions list
const editor1 = useVizelEditor({
  extensions: [createVizelFindReplaceExtension()],
});
const editor2 = useVizelEditor({
  extensions: [createVizelFindReplaceExtension()],
});

Framework-Specific Issues

React Hydration Errors

When you use SSR (Next.js, Remix), you must render the editor client-side only:

tsx
// Next.js
'use client';

import dynamic from 'next/dynamic';

const Editor = dynamic(() => import('./Editor'), { ssr: false });

export default function Page() {
  return <Editor />;
}
tsx
// Remix
import { ClientOnly } from 'remix-utils/client-only';

export default function Page() {
  return (
    <ClientOnly fallback={<div>Loading editor...</div>}>
      {() => <Editor />}
    </ClientOnly>
  );
}

Vue Reactivity Caveats

The composable wraps the editor instance in shallowRef for performance. Access .value when needed:

vue
<script setup lang="ts">
import { useVizelEditor } from '@vizel/vue';
import { watch } from 'vue';

const editor = useVizelEditor({});

// Watch editor changes
watch(
  () => editor.value,
  (newEditor) => {
    if (newEditor) {
      console.log('Editor ready');
    }
  }
);

// Access editor methods
const handleSave = () => {
  if (editor.value) {
    const content = editor.value.getJSON();
    // Save content
  }
};
</script>

Svelte Compilation Errors

Make sure you use Svelte 5 with the runes feature:

javascript
// svelte.config.js
export default {
  compilerOptions: {
    runes: true,
  },
};

For TypeScript, use the $props() rune:

svelte
<script lang="ts">
  import type { Editor } from '@tiptap/core';

  let { editor }: { editor: Editor | null } = $props();
</script>

Debugging Tips

Browser DevTools

  1. Inspect editor state:
javascript
// In browser console
const editor = document.querySelector('.vizel-editor').__vizelEditor;
console.log(editor.getJSON());
console.log(editor.state);
  1. Check registered extensions:
javascript
console.log(editor.extensionManager.extensions.map(e => e.name));
  1. Monitor transactions:
typescript
const editor = useVizelEditor({
  onTransaction: ({ transaction }) => {
    console.log('Transaction:', transaction);
    console.log('Steps:', transaction.steps);
  },
});

Editor State Inspection

Use getVizelEditorState for debugging:

typescript
import { getVizelEditorState } from '@vizel/core';

const debugEditor = () => {
  if (!editor) return;

  const state = getVizelEditorState(editor);
  console.table({
    focused: state.isFocused,
    empty: state.isEmpty,
    canUndo: state.canUndo,
    canRedo: state.canRedo,
    characters: state.characterCount,
    words: state.wordCount,
  });
};

ProseMirror DevTools

Install ProseMirror DevTools for advanced debugging:

typescript
import { applyDevTools } from 'prosemirror-dev-tools';

const editor = useVizelEditor({
  onCreate: ({ editor }) => {
    if (process.env.NODE_ENV === 'development') {
      applyDevTools(editor.view);
    }
  },
});

Logging Transactions

Track all document changes:

typescript
const editor = useVizelEditor({
  onTransaction: ({ transaction, editor }) => {
    if (transaction.docChanged) {
      console.group('Document Changed');
      console.log('From:', transaction.before.toJSON());
      console.log('To:', editor.state.doc.toJSON());
      console.log('Steps:', transaction.steps.map(s => s.toJSON()));
      console.groupEnd();
    }
  },
});

Getting Help

If you encounter an issue not covered here:

  1. Search existing issues: GitHub Issues
  2. Check Tiptap documentation: Tiptap Docs
  3. Open a new issue: Include:
    • Vizel version
    • Framework and version
    • Minimal reproduction code
    • Expected vs actual behavior
    • Browser and OS

Next Steps

Released under the MIT License.