moeru-ai/airi · warning

Failed to process markdown:

Error message

Failed to process markdown:

What it means

The markdown-renderer component re-processes content on every props.content change by calling useMarkdown().processSync (markdown-it) and then DOMPurify.sanitize, both inside one try/catch. If either throws, the watcher logs this warning and blanks processedContent, so the rendered div shows nothing. It is a caught, non-fatal rendering failure, not a crash.

Source

Thrown at packages/stage-ui/src/components/markdown/markdown-renderer.vue:43

  catch (error) {
    console.warn('Failed to process markdown with syntax highlighting, using fallback:', error)
  }
}

function processContent() {
  const content = props.content
  const requestId = ++processRequestId

  if (!content) {
    processedContent.value = ''
    return
  }

  try {
    processedContent.value = DOMPurify.sanitize(processSync(content))
  }
  catch (error) {
    console.warn('Failed to process markdown:', error)
    processedContent.value = ''
    return
  }

  if (/`{3,}/.test(content))
    void processRichContent(content, requestId)
}

watch(() => props.content, processContent, { immediate: true })
</script>

<template>
  <div
    :class="props.class"
    class="markdown-content"
    v-html="processedContent"
  />
</template>

View on GitHub (pinned to 677329427f)

Solutions

  1. Read the logged error object to identify which stage threw (processSync vs DOMPurify.sanitize)
  2. If rendering outside a browser, give DOMPurify a DOM via createDOMPurify(window) with jsdom, or only render/sanitize on the client
  3. Audit custom markdown-it plugins used by useMarkdown and wrap their transform logic in its own try/catch
  4. Render an escaped plain-text fallback instead of an empty string so users still see raw content

Example fix

// before
try {
  processedContent.value = DOMPurify.sanitize(processSync(content))
}
catch (error) {
  console.warn('Failed to process markdown:', error)
  processedContent.value = ''
  return
}

// after
try {
  processedContent.value = DOMPurify.sanitize(processSync(content))
}
catch (error) {
  console.warn('Failed to process markdown:', error)
  // keep the text visible instead of dropping it entirely
  processedContent.value = ''
  rawFallback.value = content
}
Defensive patterns

Strategy: fallback

Validate before calling

if (typeof props.content !== 'string' || props.content.length === 0) {
  processedContent.value = ''
  return
}
if (typeof DOMPurify.sanitize !== 'function') {
  // no DOM available (SSR/node): skip processing
  return
}

Type guard

function isRenderableMarkdown(value: unknown): value is string {
  return typeof value === 'string' && value.trim().length > 0
}

Try / catch

catch (error) {
  console.warn('Failed to process markdown:', error, 'content head:', content.slice(0, 120))
  processedContent.value = ''
  textFallback.value = content // show escaped raw text instead of nothing
}

Prevention

When it happens

Trigger: props.content changes and processSync(content) throws (a markdown-it plugin erroring on unusual syntax) or DOMPurify.sanitize throws (running in an environment with no DOM, e.g. SSR/prerender or a plain Node test without jsdom). The watch fires immediately: true, so the first render can already hit it.

Common situations: Rendering user- or LLM-generated markdown during SSR or in Vitest with a happy-dom/jsdom-less environment; a custom markdown-it plugin (containers, highlighters) throwing on specific input; content containing malformed HTML entities that trips sanitization.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/51bec7fdac44090f. Report an issue: GitHub.