
本文详解 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 封装将具备真正的健壮性——既能清晰定位错误源头,又能优雅降级,避免静默失败或不可预测的崩溃。