【前端面试小册】JS-第9节:try-catch 进阶与异常处理

一、错误捕获的常见误区:同步 vs 异步

1.1 核心问题

在日常开发中,try...catch 是常用的错误处理方式。但很多开发者容易忽略一个关键点:try...catch 只能捕获同步代码中的错误,无法捕获异步操作中的错误

类比理解:就像你站在门口等快递,如果快递员在门口直接给你(同步),你能立即收到。但如果快递员把包裹放在快递柜,等你去取的时候(异步),你已经不在门口了,自然收不到。

1.2 面试题一:setTimeout 中的错误

题目:以下代码有错吗?能捕获到错误吗?

try {
  setTimeout(() => {
    throw new Error('err');
  }, 200);
} catch (err) {
  console.log(err);
}

答案:❌ 无法捕获错误

原因分析

graph TD
    A[try 块开始] --> B[注册 setTimeout]
    B --> C[try 块结束]
    C --> D[catch 块执行完毕]
    D --> E[200ms 后]
    E --> F[setTimeout 回调执行]
    F --> G[抛出错误]
    G --> H[错误无法被捕获]

关键理解

  • setTimeout异步操作,回调函数在事件循环的下一个轮次才执行
  • 当回调函数执行时,try...catch 已经执行完毕
  • 异步回调中的错误会直接抛到全局,导致未捕获的异常

正确做法

// ✅ 方式一:在异步回调内部使用 try-catch
new Promise((resolve, reject) => {
  setTimeout(() => {
    try {
      throw new Error('err');
    } catch (err) {
      reject(err);  // 将错误传递给 Promise
    }
  }, 200);
})
  .then(() => {
    // 正常逻辑
  })
  .catch((err) => {
    console.log(err);  // ✅ 捕获错误
  });

// ✅ 方式二:使用 Promise 的 reject
new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('err'));  // 直接 reject
  }, 200);
})
  .catch((err) => {
    console.log(err);  // ✅ 捕获错误
  });

1.3 面试题二:Promise 链中的错误

题目:如下代码有错吗?能捕获到错误吗?

try {
  Promise.resolve().then(() => {
    throw new Error('err');
  });
} catch (err) {
  console.log(err);
}

答案:❌ 无法捕获错误

原因分析

  • Promise.then() 是异步操作,回调函数在微任务队列中执行
  • try...catch 只能捕获同步错误和 await 后的异步错误
  • Promise 链中的错误必须使用 .catch()await + try/catch 捕获

正确做法

// ✅ 方式一:使用 .catch()
Promise.resolve()
  .then(() => {
    throw new Error('err');
  })
  .catch((err) => {
    console.log(err);  // ✅ 这里会捕捉到错误
  });

// ✅ 方式二:使用 async/await + try/catch
async function handleError() {
  try {
    await Promise.resolve().then(() => {
      throw new Error('err');
    });
  } catch (err) {
    console.log(err);  // ✅ 这里会捕捉到错误
  }
}

handleError();

执行流程对比

graph TD
    A[Promise.then 注册回调] --> B[try 块结束]
    B --> C[微任务队列执行]
    C --> D[回调函数执行]
    D --> E[抛出错误]
    E --> F{使用 catch}
    F -->|是| G[错误被捕获]
    F -->|否| H[错误抛到全局]

二、异常捕获的扩展应用

2.1 捕获未处理的 Promise 错误

对于未捕获的 Promise 错误,可以通过全局监听 unhandledrejection 事件来捕获:

// ✅ 浏览器环境
window.addEventListener('unhandledrejection', function (event) {
  console.error('未处理的 Promise 错误:', event.reason);
  // 可以在这里发送错误报告到监控平台
  event.preventDefault();  // 阻止默认的错误输出
});

// ✅ Node.js 环境
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise rejection:', reason);
});

使用场景

  • 全局错误监控和上报
  • 开发环境下的错误提示
  • 生产环境的错误日志收集

2.2 捕获其他未捕获的异常

使用 window.onerror 捕获未被处理的同步或异步错误:

window.onerror = function (message, source, lineno, colno, error) {
  console.error('捕获到错误:', {
    message,    // 错误信息
    source,    // 发生错误的文件
    lineno,    // 行号
    colno,     // 列号
    error      // Error 对象
  });
  
  // 返回 true 可以阻止默认的错误处理
  return true;
};

注意window.onerror 只能捕获:

  • ✅ 同步错误
  • ✅ 部分异步错误(如 setTimeout 中的错误)
  • ❌ 无法捕获 Promise rejection(需要使用 unhandledrejection

2.3 资源加载错误处理

通过监听 error 事件捕获资源加载失败,并在失败时进行重新加载:

// ✅ 捕获资源加载错误
document.addEventListener('error', function (e) {
  if (e.target.tagName === 'IMG') {
    console.error('图片加载失败:', e.target.src);
    // 重试加载图片
    e.target.src = e.target.src;
  } else if (e.target.tagName === 'SCRIPT') {
    console.error('脚本加载失败:', e.target.src);
    // 可以切换到备用 CDN
  } else if (e.target.tagName === 'LINK') {
    console.error('样式表加载失败:', e.target.href);
  }
}, true);  // 使用捕获阶段

常见资源类型

  • IMG:图片资源
  • SCRIPT:JavaScript 文件
  • LINK:CSS 样式表
  • VIDEO / AUDIO:音视频资源
  • IFRAME:嵌入的外部页面

2.4 CDN 资源加载失败处理

针对 CDN 资源,可以在资源加载失败时重试或切换到备用 CDN:

function loadImage(src, fallbackSrc) {
  const img = new Image();
  
  img.onload = function() {
    console.log('图片加载成功');
    document.body.appendChild(img);
  };
  
  img.onerror = function() {
    console.error(`图片加载失败: ${src},使用备用 CDN: ${fallbackSrc}`);
    img.src = fallbackSrc;  // 使用备用图片地址
  };
  
  img.src = src;
}

// 使用示例:图片加载时使用备用 CDN
loadImage(
  'https://primary-cdn.com/image.jpg',
  'https://backup-cdn.com/image.jpg'
);

进阶:实现自动重试机制

function loadImageWithRetry(src, maxRetries = 3) {
  return new Promise((resolve, reject) => {
    let retries = 0;
    
    function attemptLoad() {
      const img = new Image();
      
      img.onload = () => resolve(img);
      img.onerror = () => {
        retries++;
        if (retries < maxRetries) {
          console.log(`重试加载图片 (${retries}/${maxRetries}):`, src);
          attemptLoad();
        } else {
          reject(new Error(`图片加载失败,已重试 ${maxRetries} 次`));
        }
      };
      
      img.src = src;
    }
    
    attemptLoad();
  });
}

// 使用示例
loadImageWithRetry('https://example.com/image.jpg', 3)
  .then(img => document.body.appendChild(img))
  .catch(err => console.error(err));

三、try-catch 的最佳实践

3.1 避免滥用 try-catch

原则:不要使用 try-catch 控制正常流程,应该仅用于不可预见的异常。

// ❌ 不好的做法:用 try-catch 控制流程
function getUser(id) {
  try {
    const user = users.find(u => u.id === id);
    if (!user) {
      throw new Error('用户不存在');
    }
    return user;
  } catch (error) {
    return null;
  }
}

// ✅ 好的做法:正常流程用 if-else
function getUser(id) {
  const user = users.find(u => u.id === id);
  if (!user) {
    return null;  // 或返回默认值
  }
  return user;
}

3.2 局部捕获错误

只在需要捕获的代码块中使用 try-catch,而非整个函数,减少错误排查难度。

// ❌ 不好的做法:整个函数包裹
function processData(data) {
  try {
    const parsed = JSON.parse(data);
    const processed = process(parsed);
    const formatted = format(processed);
    return formatted;
  } catch (error) {
    console.error('处理失败');
    return null;
  }
}

// ✅ 好的做法:局部捕获
function processData(data) {
  let parsed;
  try {
    parsed = JSON.parse(data);
  } catch (error) {
    console.error('JSON 解析失败:', error);
    return null;
  }
  
  // 其他处理逻辑不需要 try-catch
  const processed = process(parsed);
  const formatted = format(processed);
  return formatted;
}

3.3 使用 finally 进行清理操作

finally 块无论是否有错误都会执行,用于资源清理等操作。

function readFile(filename) {
  let fileHandle;
  try {
    fileHandle = openFile(filename);
    const content = readContent(fileHandle);
    return content;
  } catch (error) {
    console.error('读取文件失败:', error);
    throw error;  // 重新抛出错误
  } finally {
    // ✅ 无论成功或失败,都要关闭文件
    if (fileHandle) {
      closeFile(fileHandle);
    }
  }
}

常见清理场景

  • 关闭文件句柄
  • 释放数据库连接
  • 清理定时器
  • 重置状态

3.4 提供有意义的错误信息

// ❌ 不好的做法:错误信息不明确
try {
  const data = JSON.parse(jsonString);
} catch (error) {
  console.error('错误');
}

// ✅ 好的做法:提供详细的错误信息
try {
  const data = JSON.parse(jsonString);
} catch (error) {
  console.error('JSON 解析失败:', {
    error: error.message,
    input: jsonString.substring(0, 100),  // 只显示前100个字符
    stack: error.stack
  });
  throw new Error(`无法解析 JSON: ${error.message}`);
}

3.5 错误分类处理

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    
    if (!response.ok) {
      // 根据状态码分类处理
      if (response.status === 404) {
        throw new Error('用户不存在');
      } else if (response.status === 500) {
        throw new Error('服务器错误');
      } else {
        throw new Error(`请求失败: ${response.status}`);
      }
    }
    
    return await response.json();
  } catch (error) {
    // 网络错误
    if (error.name === 'TypeError' && error.message.includes('fetch')) {
      console.error('网络连接失败');
      throw new Error('请检查网络连接');
    }
    // 其他错误
    throw error;
  }
}

四、异步错误处理的最佳实践

4.1 Promise 链式调用

// ✅ 在 Promise 链的末尾添加 catch
fetch('/api/data')
  .then(response => response.json())
  .then(data => processData(data))
  .then(result => displayResult(result))
  .catch(error => {
    // 统一处理所有错误
    console.error('操作失败:', error);
    showErrorMessage(error.message);
  });

4.2 async/await 错误处理

// ✅ 使用 async/await + try/catch
async function fetchAndProcess() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    const processed = await processData(data);
    return processed;
  } catch (error) {
    console.error('处理失败:', error);
    // 可以返回默认值或重新抛出
    throw error;
  }
}

4.3 错误边界模式

// ✅ 创建错误边界函数
function withErrorBoundary(fn, fallback) {
  return async function(...args) {
    try {
      return await fn(...args);
    } catch (error) {
      console.error('错误边界捕获:', error);
      return fallback ? fallback(error) : null;
    }
  };
}

// 使用示例
const safeFetch = withErrorBoundary(
  fetch,
  (error) => ({ error: error.message, data: null })
);

const result = await safeFetch('/api/data');

五、面试要点总结

核心知识点

  1. try-catch 的局限性

    • ✅ 只能捕获同步错误
    • ✅ 可以捕获 await 后的异步错误
    • ❌ 无法捕获 Promise 链中的错误(需用 .catch()
    • ❌ 无法捕获 setTimeout 等异步回调中的错误
  2. 异步错误处理方式

    • Promise 链:使用 .catch()
    • async/await:使用 try/catch + await
    • 全局监听:unhandledrejection 事件
  3. 最佳实践

    • 避免滥用 try-catch 控制流程
    • 局部捕获,提供有意义的错误信息
    • 使用 finally 进行资源清理
    • 分类处理不同类型的错误

常见面试题

Q1: try-catch 能捕获异步错误吗?

答:需要分情况

  • ❌ 不能捕获 setTimeoutPromise.then() 等异步回调中的错误
  • ✅ 可以捕获 await 后的异步错误
  • ✅ 可以捕获同步错误

Q2: 如何捕获 Promise 中的错误?

答:

  1. 使用 .catch() 方法
  2. 使用 async/await + try/catch
  3. 全局监听 unhandledrejection 事件

Q3: 如何实现资源加载失败的重试机制?

答:

  1. 监听 error 事件
  2. 根据资源类型(IMG、SCRIPT 等)判断
  3. 实现重试逻辑或切换到备用资源
  4. 使用 Promise 封装,支持多次重试

实战建议

  • ✅ 异步操作统一使用 Promise 或 async/await
  • ✅ 在 Promise 链末尾添加 .catch()
  • ✅ 在应用入口添加全局错误监听
  • ✅ 提供有意义的错误信息和错误分类
  • ✅ 使用 finally 确保资源清理
#前端面试##前端##面试问题记录#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务