JavaScript疑难Bug?试试这8个排查技巧
一、使用console.trace()追踪调用栈
function functionA() { functionB(); }
function functionB() { functionC(); }
function functionC() { console.trace('追踪调用路径'); // 这里有个错误但不知道从哪里调用的 throw new Error('示例错误'); }
functionA();
二、利用debugger语句和浏览器开发者工具
function calculateTotal(items) {
let total = 0;
debugger; // 执行会在这里暂停
for (let i = 0; i <= items.length; i++) { // 故意错误:<= 应该是 <
total += items[i].price;
}
return total;
}
const cart = [{price: 10}, {price: 20}, {price: 30}];
console.log(calculateTotal(cart));
三、使用try-catch捕获异步错误
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('获取用户数据失败:', error);
// 可以在这里添加错误上报逻辑
Sentry.captureException(error);
throw error; // 重新抛出以便外部处理
}
}
// 使用示例
fetchUserData(123)
.then(data => console.log(data))
.catch(err => console.log('处理错误:', err));
四、全局错误处理
window.onerror = function(message, source, lineno, colno, error) {
console.error('全局捕获的异常:', {
message, // 错误信息
source, // 发生错误的脚本URL
lineno, // 行号
colno, // 列号
error // Error对象(包含调用栈)
});
// 可以在这里发送错误到监控系统
sendErrorToServer({
type: 'unhandled_error',
details: { message, source, lineno, colno },
stack: error?.stack
});
// 返回true可以阻止浏览器默认错误处理
return true;
};
// 触发示例
undefinedFunction(); // 会触发window.onerror
五、Promise 未捕获异常处理
window.addEventListener('unhandledrejection', (event) => {
const error = event.reason;
const isExpectedError = error?.isExpected; // 自定义错误标识
if (!isExpectedError) {
sendToErrorMonitoring({
type: 'unhandled_rejection',
error: error?.toString(),
stack: error?.stack,
timestamp: new Date().toISOString()
});
}
// 对重要错误显示用户通知
if (error?.isCritical) {
showErrorToast('操作失败,请稍后重试');
}
});
// 触发示例
Promise.reject(new Error('异步操作失败'));
new Promise(() => { throw new Error('构造器中抛出异常') });
六、自定义错误类型
class AppError extends Error {
constructor(message, statusCode = 500, details = {}) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.details = details;
this.isOperational = true; // 标记为可预期的操作错误
// 保留原始调用栈
Error.captureStackTrace(this, this.constructor);
}
}
// 使用示例
try {
throw new AppError('用户未找到', 404, { userId: 123 });
} catch (error) {
console.error(`${error.name}: ${error.message}`);
console.error('状态码:', error.statusCode);
console.error('详情:', error.details);
}
七、错误降级处理
async function fetchWithCacheFallback(url, cacheKey, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
// 更新缓存
localStorage.setItem(cacheKey, JSON.stringify({
data,
timestamp: Date.now()
}));
return data;
} catch (error) {
console.warn('网络请求失败,尝试使用缓存:', error.message);
// 尝试从缓存读取
const cached = localStorage.getItem(cacheKey);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < 24 * 60 * 60 * 1000) { // 1天内缓存有效
return data;
}
}
// 缓存不可用则抛出错误
throw new Error('无法获取数据且无有效缓存');
}
}
// 使用示例
try {
const products = await fetchWithCacheFallback(
'/api/products',
'products-cache'
);
displayProducts(products);
} catch (finalError) {
showEmptyState('暂时无法显示产品列表');
}
八、异步错误处理链优化
1.日志等级标准定义
// 日志级别常量(符合RFC5424标准)
const LogLevel = {
EMERGENCY: 0, // 系统不可用
ALERT: 1, // 必须立即采取措施
CRITICAL: 2, // 严重情况
ERROR: 3, // 错误事件但系统仍能运行
WARNING: 4, // 警告事件
NOTICE: 5, // 正常但重要的事件
INFO: 6, // 常规信息
DEBUG: 7 // 调试信息
};
// 反向映射用于输出
const LevelNames = Object.fromEntries(
Object.entries(LogLevel).map(([k, v]) => [v, k])
);
2.日志记录器核心实现
class Logger {
constructor(options = {}) {
this.minLevel = options.minLevel || LogLevel.INFO;
this.transports = options.transports || [new ConsoleTransport()];
this.context = options.context || {};
this.sensitiveFields = options.sensitiveFields || ['password', 'token'];
}
// 核心记录方法
log(level, message, meta = {}) {
if (level > this.minLevel) return;
const entry = {
timestamp: new Date().toISOString(),
level,
levelName: LevelNames[level],
message,
...this._sanitize(meta),
context: { ...this.context }
};
this.transports.forEach(transport => {
try {
transport.log(entry);
} catch (transportError) {
console.error('日志传输失败:', transportError);
}
});
}
// 数据脱敏处理
_sanitize(data) {
if (typeof data !== 'object' || data === null) return data;
const sanitized = { ...data };
this.sensitiveFields.forEach(field => {
if (sanitized[field]) {
sanitized[field] = '*****';
}
});
return sanitized;
}
// 快捷方法
emergency(message, meta) { this.log(LogLevel.EMERGENCY, message, meta); }
alert(message, meta) { this.log(LogLevel.ALERT, message, meta); }
critical(message, meta) { this.log(LogLevel.CRITICAL, message, meta); }
error(message, meta) { this.log(LogLevel.ERROR, message, meta); }
warning(message, meta) { this.log(LogLevel.WARNING, message, meta); }
notice(message, meta) { this.log(LogLevel.NOTICE, message, meta); }
info(message, meta) { this.log(LogLevel.INFO, message, meta); }
debug(message, meta) { this.log(LogLevel.DEBUG, message, meta); }
// 创建子记录器(继承上下文)
child(context) {
return new Logger({
...this,
context: { ...this.context, ...context }
});
}
}
3.日志控制台传输
class ConsoleTransport {
constructor(options = {}) {
this.format = options.format || 'text'; // 'text' | 'json'
this.colors = {
[LogLevel.EMERGENCY]: '\x1b[41m', // 红底
[LogLevel.ALERT]: '\x1b[35m', // 紫色
[LogLevel.CRITICAL]: '\x1b[31m', // 红色
[LogLevel.ERROR]: '\x1b[91m', // 亮红
[LogLevel.WARNING]: '\x1b[33m', // 黄色
[LogLevel.NOTICE]: '\x1b[36m', // 青色
[LogLevel.INFO]: '\x1b[32m', // 绿色
[LogLevel.DEBUG]: '\x1b[90m', // 灰色
reset: '\x1b[0m'
};
}
log(entry) {
if (this.format === 'json') {
console.log(JSON.stringify(entry));
return;
}
const color = this.colors[entry.level] || '';
const levelName = entry.levelName.padEnd(8);
const prefix = `${color}[${entry.timestamp}] ${levelName}:${this.colors.reset}`;
console.log(`${prefix} ${entry.message}`);
if (Object.keys(entry.context).length > 0) {
console.log(' Context:', entry.context);
}
if (Object.keys(entry).filter(k => !['timestamp','level','levelName','message','context'].includes(k)).length > 0) {
console.log(' Metadata:',
Object.fromEntries(
Object.entries(entry)
.filter(([k]) => !['timestamp','level','levelName','message','context'].includes(k))
);
}
}
}
2926

被折叠的 条评论
为什么被折叠?



