如何在 JavaScript 中正确处理 IndexedDB API 的错误?

2次阅读

如何在 JavaScript 中正确处理 IndexedDB API 的错误?

本文详解 indexeddb 错误处理的最佳实践,涵盖 `idbopenrequest` 与普通 `idbrequest` 的错误捕获差异、事务生命周期管理、`onblocked`/`onversionchange` 等关键事件的必要监听,以及避免 promise 微任务导致事务提前失效等常见陷阱。

IndexedDB 的错误处理机制具有层级性且易被误解——错误不会自动“冒泡”到上层对象(如数据库或事务),而是严格绑定在触发错误的具体请求(IDBRequest) 上。因此,必须在每个请求级别显式监听 onError,而非依赖 db.onerror 或 transaction.onerror(后者甚至不存在)。下面从核心原则、代码重构和关键注意事项三方面展开说明。

✅ 核心错误处理原则

  • 所有操作都基于请求(IDBRequest):无论是 indexedDB.open()(返回 IDBOpenRequest)、store.get() 还是 transaction.commit(),其本质都是 IDBRequest 的子类。错误始终通过该请求实例的 error 属性暴露。
  • db.onerror 是无效的:IDBDatabase 对象没有 onerror 属性。你注释掉的 request.result.onerror = … 实际上会报错(TypeError: Cannot set Property onerror of # which has only a getter)。错误只能在请求或事务层面捕获。
  • 事务级错误需通过事件目标访问:事务本身不直接抛出错误,但当其中任一请求失败时,事务的 onerror 会被触发。此时应通过 Event.target(即出错的请求)获取 error:
    const transaction = db.transaction('store', 'readwrite'); transaction.onerror = (event) => {   const failedRequest = event.target as IDBRequest;   console.error('Transaction failed:', failedRequest.error);   // 注意:此处不能直接 reject,需确保事务已终止 };
  • 永远不要在 onsuccess 中关闭数据库:db.close() 必须在所有相关请求完成之后调用。你在 GetAllItems 中 db.close() 放在 store.getAll() 之前,会导致请求立即中止(InvalidStateError)。正确做法是监听 datarequest.onsuccess 后再关闭。

✅ 重构后的健壮实现(typescript

export default class IndexedDBStorage {   #name: string;    constructor(name: string) {     this.#name = name;   }    private async getDB(): Promise {     return new Promise((resolve, reject) => {       const request = indexedDB.open(this.#name, 1);        // ? 关键:open 请求的错误(权限拒绝、磁盘满等)       request.onerror = (event) => {         const error = (event as unknown as { error?: DOMException }).error;         console.error('Failed to open database:', error?.message || event);         reject(new Error(`Open DB failed: ${error?.name || 'Unknown'}`));       };        // ⚠️ 关键:数据库被阻塞(其他标签页未响应 versionchange)       request.onblocked = () => {         alert('Database is blocked. Please close other tabs and reload.');         reject(new Error('Database blocked'));       };        // ?️ 升级逻辑(仅当版本变更时触发)       request.onupgradeneeded = (event) => {         const db = request.result;         const oldVersion = event.oldVersion;          if (!db.objectStoreNames.contains(this.#name)) {           db.createObjectStore(this.#name, { keyPath: 'id', autoIncrement: true });         }       };        // ✅ 成功打开后,设置数据库级 versionchange 监听(强制刷新旧页面)       request.onsuccess = () => {         const db = request.result;         db.onversionchange = () => {           db.close();           alert('Database updated. Reloading...');           location.reload();         };         resolve(db);       };     });   }    async getAllItems(): Promise {     const db = await this.getDB();     return new Promise((resolve, reject) => {       // ? 创建事务并监听其错误(推荐方式)       const transaction = db.transaction(this.#name, 'readonly');       transaction.onerror = (event) => {         const failedRequest = event.target as IDBRequest;         console.error('Transaction error:', failedRequest.error);         reject(failedRequest.error || new Error('Transaction failed'));       };        const store = transaction.objectStore(this.#name);       const request = store.getAll();        request.onsuccess = () => {         // ✅ 事务自动完成,此时可安全关闭数据库         db.close();         resolve(request.result as T[]);       };        request.onerror = (event) => {         // ❗ 即使监听了 transaction.onerror,仍建议在此处也处理(防御性编程)         console.error('GetAll request failed:', request.error);         reject(request.error || new Error('GetAll failed'));       };     });   }    // ✅ 示例:带错误处理的写入方法   async addItem(item: T): Promise {     const db = await this.getDB();     return new Promise((resolve, reject) => {       const transaction = db.transaction(this.#name, 'readwrite');       transaction.onerror = (event) => {         const failedRequest = event.target as IDBRequest;         reject(failedRequest.error || new Error('Write transaction failed'));       };        const store = transaction.objectStore(this.#name);       const request = store.add(item);        request.onsuccess = () => {         db.close();         resolve();       };       request.onerror = () => reject(request.error);     });   } }

⚠️ 关键注意事项与避坑指南

  • 禁止过早关闭数据库:db.close() 必须在所有 IDBRequest.onsuccess 触发后执行。若在请求发出后立即调用,会导致 InvalidStateError(”The transaction has finished”)。
  • Promise 微任务陷阱:避免将单个 IDBRequest(如 store.get())包装成 Promise 并 await,因为 IndexedDB 事务在当前事件循环微任务结束后自动终止。若 await 的 Promise 在事务结束之后才 resolve,后续操作将失败。推荐包装整个事务(如上例 getAllItems),或使用同步回调模式。
  • onblocked 不可忽略:当新版本数据库打开时,若旧版本数据库未响应 onversionchange(例如用户未关闭其他标签页),onblocked 会永久挂起 open 请求。必须显式处理,否则应用卡死。
  • 类型安全提示:request.error 类型为 DOMException | NULL,而非字符串。直接拼接 request.error?.code 可能为 undefined;应使用 error?.name 或 error?.message。
  • 生产环境建议:将 alert() 替换为非阻塞 ui 提示(如 Toast),并将错误上报至监控服务(如 sentry),而非仅 console.error。

遵循以上实践,你的 IndexedDB 封装将具备真正的健壮性——既能清晰定位错误源头,又能优雅降级,避免静默失败或不可预测的崩溃。

text=ZqhQzanResources