You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用async/await改写Node.js Express登录逻辑并优化错误处理

如何用async/await精简Express登录逻辑并实现精准错误处理

这个问题我之前也碰到过——用async/await摆脱回调地狱后,又掉进了错误处理的坑里。既要保留async/await的简洁性,又要精准识别每个步骤的错误,下面几个方案能完美解决你的问题:

方案1:自定义错误类型(最规范的长期维护方案)

创建不同的错误类来区分错误来源和类型,这样在catch块里可以通过instanceof快速判断错误类型,返回对应的响应。

首先定义自定义错误类:

class ValidationError extends Error {
  constructor(message, details) {
    super(message);
    this.name = 'ValidationError';
    this.statusCode = 403;
    this.details = details;
  }
}

class DbError extends Error {
  constructor(message, details) {
    super(message);
    this.name = 'DbError';
    this.statusCode = 500;
    this.details = details;
  }
}

class AuthError extends Error {
  constructor(message) {
    super(message);
    this.name = 'AuthError';
    this.statusCode = 403;
  }
}

class JwtError extends Error {
  constructor(message, details) {
    super(message);
    this.name = 'JwtError';
    this.statusCode = 500;
    this.details = details;
  }
}

然后把原来的回调式工具函数转成Promise风格(用util.promisify或手动封装),并抛出对应自定义错误:

const util = require('util');
// 把回调函数转成Promise
const checkIfEmailAndPasswordAreSet = util.promisify(originalCheckFn);
const findUserByEmail = util.promisify(originalFindUserFn);

// 手动封装示例(如果原函数不适合直接promisify)
function checkUserPassword(user, password) {
  return new Promise((resolve, reject) => {
    originalCheckPasswordFn(user, password, (error, result) => {
      if (error) {
        return reject(new DbError('bcrypt error', error));
      }
      resolve(result);
    });
  });
}

最后重写登录逻辑,只用一个try/catch:

async function loginHandler(req, res) {
  const { email, password } = req.body;
  try {
    await checkIfEmailAndPasswordAreSet(email, password);
    const user = await findUserByEmail(email);
    
    if (!user) throw new AuthError('user not found');
    
    const result = await checkUserPassword(user, password);
    if (!result) throw new AuthError('incorrect password');
    
    await updateUserLastLoggedIn(user);
    const token = await generateSessionToken(user);
    
    return successResponse(user, token);
  } catch (e) {
    // 根据错误类型返回对应响应
    if (e instanceof ValidationError || e instanceof AuthError) {
      return errorResponse(e.statusCode, e.message, e.details);
    }
    if (e instanceof DbError || e instanceof JwtError) {
      return errorResponse(e.statusCode, e.message, e.details);
    }
    // 兜底处理未知错误
    return errorResponse(500, 'unknown error', e);
  }
}

方案2:给错误添加自定义属性(轻量快速方案)

如果不想创建多个错误类,可以直接给Error实例添加自定义属性,比如source、statusCode,在catch里根据这些属性判断:

修改工具函数的Promise封装:

function checkIfEmailAndPasswordAreSet(email, password) {
  return new Promise((resolve, reject) => {
    originalCheckFn(email, password, (error, response) => {
      if (error) {
        const err = new Error('validation error');
        err.source = 'checkIfEmailAndPasswordAreSet';
        err.statusCode = 403;
        err.details = error;
        return reject(err);
      }
      resolve(response);
    });
  });
}

然后在catch块里处理:

async function loginHandler(req, res) {
  const { email, password } = req.body;
  try {
    // ... 同之前的业务逻辑 ...
  } catch (e) {
    // 处理自定义抛出的认证错误
    if (e.message === 'user not found' || e.message === 'incorrect password') {
      return errorResponse(403, e.message);
    }
    // 根据source区分工具函数错误
    switch(e.source) {
      case 'checkIfEmailAndPasswordAreSet':
        return errorResponse(e.statusCode, e.message, e.details);
      case 'findUserByEmail':
      case 'updateUserLastLoggedIn':
        return errorResponse(500, 'db error', e.details);
      case 'checkUserPassword':
        return errorResponse(500, 'bcrypt error', e.details);
      case 'generateSessionToken':
        return errorResponse(500, 'jwt error', e.details);
      default:
        return errorResponse(500, 'unknown error', e);
    }
  }
}

方案3:封装异步操作返回{ error, data }(无try/catch直观方案)

如果不想用try/catch,可以把每个异步操作封装成返回{ error, data }的函数,直接判断error即可:

async function wrapAsync(fn, ...args) {
  try {
    const data = await fn(...args);
    return { error: null, data };
  } catch (error) {
    return { error, data: null };
  }
}

async function loginHandler(req, res) {
  const { email, password } = req.body;
  
  const { error: validationError } = await wrapAsync(checkIfEmailAndPasswordAreSet, email, password);
  if (validationError) {
    return errorResponse(403, 'validation error', validationError);
  }
  
  const { error: dbError, data: user } = await wrapAsync(findUserByEmail, email);
  if (dbError) return errorResponse(500, 'db error', dbError);
  if (!user) return errorResponse(403, 'user not found');
  
  const { error: bcryptError, data: result } = await wrapAsync(checkUserPassword, user, password);
  if (bcryptError) return errorResponse(500, 'bcrypt error', bcryptError);
  if (!result) return errorResponse(403, 'incorrect password');
  
  // 后续步骤同理...
  const token = await generateSessionToken(user);
  return successResponse(user, token);
}

这个方案没有嵌套的try/catch,代码线性清晰,也能精准处理每个步骤的错误,缺点是比自定义错误类多了几行判断,但胜在直观。

总结

  • 如果项目需要长期维护,**方案1(自定义错误类)**是最规范的,符合Node.js错误处理最佳实践,扩展性强。
  • 如果是小型项目或快速迭代,方案2或3更轻量,不需要额外定义类,能快速实现需求。

内容的提问来源于stack exchange,提问作者Kasheftin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:42:55