错误处理

在应用运行过程中,组件执行、生命周期钩子、副作用等都可能抛出异常。Vitarx 提供了分层级的错误处理机制,让你可以在不同层级捕获和处理错误,避免一个组件的异常导致整个应用崩溃。

错误处理层级

Vitarx 的错误处理分为两个层级:

  1. 组件级 — 通过 onError 在组件内部捕获错误
  2. 应用级 — 通过 app.config.errorHandler 全局捕获错误

错误会从发生异常的组件开始,由内向外逐层冒泡,直到被某个处理器拦截或到达应用级。

text
子组件 onError → 父组件 onError → ... → app.config.errorHandler

app.config.errorHandler — 全局错误处理器

在创建应用时,可以通过 config.errorHandler 设置全局错误处理器。当错误冒泡到应用级别时,该处理器会被调用。

typescript
interface AppConfig {
  errorHandler?: (error: unknown, info: ErrorInfo) => void
}
  • error:捕获到的错误对象
  • info:错误信息对象,包含 source(错误来源)和 instance(发生错误的组件实例)
tsx
import { createApp } from 'vitarx'

function App() {
  return <div>Hello</div>
}

const app = createApp(App, {
  errorHandler: (error, info) => {
    // 全局错误处理
    console.error(`[全局错误] 来源:#123;info.source}`, error)

    // 上报错误到监控平台
    // reportError(error, info)
  }
})

app.mount('#app')

如果不设置 errorHandler,Vitarx 会使用默认处理器,将错误打印到控制台。

errorHandler 也可以在创建应用后直接修改:app.config.errorHandler = myHandler

onError — 组件级错误处理器

onError 在组件内部注册错误处理函数,可以捕获当前组件及其子组件抛出的错误。

typescript
declare function onError(handler: ErrorHandler): void

type ErrorHandler = (error: unknown, info: ErrorInfo) => boolean | void
  • error:捕获到的错误对象
  • info:错误信息对象,包含 source(错误来源)和 instance(发生错误的组件实例)
  • 返回值:返回 false 表示错误已被处理,阻止错误继续冒泡;不返回或返回其他值则错误继续冒泡
tsx
import { onError, ref, dynamic } from 'vitarx'

function SafeComponent() {
  const hasError = ref(false)

  onError((error, info) => {
    console.error('组件错误:', error)
    console.error('错误来源:', info.source)
    hasError.value = true
    // 返回 false 阻止错误继续冒泡到父组件
    return false
  })

  // 使用 dynamic 包裹条件渲染,响应式数据变化时自动更新
  return dynamic(() => (hasError.value ? <div>组件出现异常,请稍后重试</div> : <div>正常内容</div>))
}

onError 必须在组件函数的顶层调用,不能在异步回调中使用。

错误冒泡机制

当组件内部发生错误时,Vitarx 会按照以下顺序处理:

  1. 先调用当前组件的 onError 处理器
  2. 如果 onError 返回 false,停止冒泡
  3. 如果 onError 未返回 false,错误冒泡到父组件的 onError
  4. 重复以上步骤,直到某个处理器返回 false 或到达根组件
  5. 如果根组件也没有拦截,错误交给 app.config.errorHandler 处理
text
子组件 A 抛出错误
  → 子组件 A 的 onError(未拦截)
    → 父组件 B 的 onError(未拦截)
      → 根组件的 onError(未拦截)
        → app.config.errorHandler

ErrorInfo 类型

ErrorInfo 是错误信息对象,包含以下字段:

typescript
interface ErrorInfo {
  /** 错误来源 */
  source: ErrorSource
  /** 抛出异常的组件实例 */
  instance: ComponentInstance
}

ErrorSource 类型

ErrorSource 是一个字符串联合类型,标识错误的具体来源:

来源说明
component:run执行函数组件时发生的错误
effect:${string}执行某个副作用时发生的错误
hook:${Lifecycle}执行生命周期钩子时发生的错误,如 hook:inithook:mounted
view:switch切换视图时发生的错误(DynamicView 发出)
view:update更新视图时发生的错误(ElementView 发出)
view:build构建视图时发生的错误(For 组件发出)

通过 info.source 可以判断错误发生的具体位置,便于针对性处理。

常见错误处理模式

模式一:全局兜底

最简单的做法是只设置全局错误处理器,统一记录和处理所有未捕获的错误:

tsx
import { createApp } from 'vitarx'

function App() {
  return <div>Hello</div>
}

const app = createApp(App, {
  errorHandler: (error, info) => {
    console.error(`[#123;info.source}]`, error)
  }
})

app.mount('#app')

模式二:错误边界

在关键组件上使用 onError 捕获子组件错误并展示降级 UI,阻止错误影响其他部分:

tsx
import { onError, ref, dynamic } from 'vitarx'

function ErrorBoundary(props: { children: any }) {
  const error = ref<unknown>(null)

  onError((err, info) => {
    error.value = err
    console.error('错误边界捕获:', err, '来源:', info.source)
    // 阻止冒泡,错误到此为止
    return false
  })

  return dynamic(() =>
    error.value !== null ? (
      <div class="error-fallback">
        <h3>组件出现异常</h3>
        <p>请刷新页面重试</p>
      </div>
    ) : (
      props.children
    )
  )
}

// 使用
function App() {
  return (
    <div>
      <ErrorBoundary>
        <MaybeBrokenComponent />
      </ErrorBoundary>
      <OtherComponent />
    </div>
  )
}

模式三:分层处理

在组件级记录错误并展示降级 UI,同时允许错误冒泡到全局进行上报:

tsx
import { onError, ref } from 'vitarx'

function SafeSection() {
  const hasError = ref(false)

  onError((error, info) => {
    // 展示降级 UI
    hasError.value = true
    // 不返回 false,让错误继续冒泡到全局处理器进行上报
  })

  // 使用三元表达式实现条件渲染
  return <>{hasError.value ? '该区域暂时不可用' : '正常内容'}</>
}

模式四:根据来源区分处理

通过 info.source 判断错误类型,进行针对性处理:

tsx
import { onError } from 'vitarx'

function MyComponent() {
  onError((error, info) => {
    if (info.source.startsWith('hook:')) {
      // 生命周期钩子错误
      console.error('生命周期错误:', error)
    } else if (info.source.startsWith('effect:')) {
      // 副作用错误
      console.error('副作用错误:', error)
    } else {
      // 其他错误
      console.error('未知错误:', error)
    }
    return false
  })

  return <div>内容</div>
}

可捕获与不可捕获的错误

onErrorapp.config.errorHandler 只能捕获框架执行上下文中发生的错误。以下场景的错误可以被捕获

  • 组件函数执行时(component:run
  • 副作用执行时(effect:*
  • 生命周期钩子执行时(hook:*
  • 视图更新时(view:update
  • 视图切换时(view:switch
  • 视图构建时(view:build

以下场景的错误不会被捕获

  • DOM 事件处理器(如 onClickonInput 中的异常)
  • setTimeoutsetInterval 等异步回调
  • Promise.then() / .catch() 回调
  • 原生事件监听器(addEventListener

对于这些场景,你需要自行使用 try/catch 处理:

tsx
function MyComponent() {
  const handleClick = () => {
    try {
      // 可能出错的操作
      riskyOperation()
    } catch (e) {
      console.error('事件处理出错:', e)
    }
  }

  return <button onClick={handleClick}>点击</button>
}

完整示例

下面是一个展示错误处理完整流程的示例,包含组件级捕获、错误冒泡和全局处理:

tsx
import { createApp, onError, onMounted, ref, dynamic } from 'vitarx'

// 可能出错的子组件——在生命周期钩子中模拟错误
function BuggyComponent() {
  const data = ref<string | null>(null)

  onMounted(() => {
    // 模拟挂载后数据加载失败
    throw new Error('数据加载失败!')
  })

  return <div>{data.value ?? '加载中...'}</div>
}

// 错误边界组件 — 捕获子组件错误并展示降级 UI
function ErrorBoundary(props: { children: any }) {
  const error = ref<unknown>(null)

  onError((err, info) => {
    error.value = err
    console.warn('[ErrorBoundary] 捕获到错误:', err)
    console.warn('[ErrorBoundary] 错误来源:', info.source)
    // 阻止冒泡,不让错误传播到更上层
    return false
  })

  return dynamic(() =>
    error.value !== null ? (
      <div class="error-fallback">
        <p>子组件发生异常,已降级显示</p>
        <button
          onClick={() => {
            error.value = null
          }}
        >
          重试
        </button>
      </div>
    ) : (
      props.children
    )
  )
}

// 根组件
function App() {
  return (
    <div>
      <h1>错误处理示例</h1>
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
      <p>组件挂载时会触发错误</p>
    </div>
  )
}

// 创建应用,设置全局错误处理器
const app = createApp(App, {
  errorHandler: (error, info) => {
    console.error('[全局错误处理器]', error)
    console.error('[全局错误处理器] 来源:', info.source)
    // 在这里可以将错误上报到监控平台
  }
})

app.mount('#app')

下一步