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

Remix-Run:延迟数据未处理Promise拒绝致应用崩溃求助

解决Remix延迟数据抛出错误导致应用崩溃且Await组件无法识别错误的问题

核心问题原因

  1. 你在服务端异步函数中直接抛出Response对象作为Promise拒绝原因,Node.js无法识别该类型的拒绝,触发ERR_UNHANDLED_REJECTION导致应用崩溃。
  2. 若提前捕获Promise错误,会将Promise转为resolved状态,@remix-run/react/Await组件无法感知错误发生。
  3. 全局process.on('unhandledRejection')粗暴忽略错误的做法会掩盖潜在问题,并非最佳实践。

解决方案

步骤1:定义自定义错误类,统一错误格式

替换直接抛出Response的方式,使用包含状态码的标准Error对象,让Node.js和Remix都能正确处理:

// 自定义应用错误类
export class AppError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'AppError';
  }
}

步骤2:修改服务端异步函数,抛出自定义错误

调整parseId和getFilesByParent,确保错误以标准Error类型抛出:

// 修改parseId
export async function parseId(value: string | undefined) {
  return await idSchema.parseAsync(value).catch(() => {
    throw new AppError(406, 'Invalid id!');
  });
}

// 修改getFilesByParent
export async function getFilesByParent(request: Request, params: Params) {
  try {
    const db = await tenantDb({ request, permissions: [UserPermissions.GetFiles] });

    const { parentId, parentType } = parentSchema.parse(params);
    const id = await http.parser.id(parentId);

    return await db.query.files
      .findMany({ where: and(eq(files.parentId, id), eq(files.parentType, parentType)) })
      .execute();
  } catch (err) {
    if (err instanceof AppError) {
      throw err;
    }
    // 其他未知错误统一转为500状态码
    throw new AppError(500, 'Internal server error');
  }
}

步骤3:Loader中保留延迟Promise的拒绝状态

无需提前await或捕获延迟数据的Promise,直接传递给defer:

export async function loader({ request, params }: LoaderFunctionArgs) {
  const files = getFilesByParent(request, {
    parentType: 'customers',
    parentId: params.id,
  });
  
  const customer = await getCustomer(request, params);

  return defer({ customer, files });
}

步骤4:前端用Await组件正确捕获错误

通过errorElement属性或React错误边界处理延迟数据的错误:

import { Await, useLoaderData } from '@remix-run/react';
import { Suspense } from 'react';

export default function CustomerPage() {
  const { customer, files } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>{customer.name}</h1>
      <Suspense fallback={<div>加载文件中...</div>}>
        <Await
          resolve={files}
          errorElement={({ error }) => (
            <div className="text-red-500">加载文件失败:{error.message}</div>
          )}
        >
          {(files) => (
            <ul className="mt-4">
              {files.map(file => (
                <li key={file.id}>{file.name}</li>
              ))}
            </ul>
          )}
        </Await>
      </Suspense>
    </div>
  );
}

步骤5:全局未处理拒绝的正确处理(可选)

不要粗暴忽略错误,而是在entry.server.tsx中添加日志记录逻辑,便于排查问题:

// entry.server.tsx
export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
  loadContext: AppLoadContext
) {
  // 全局捕获未处理Promise拒绝,仅用于日志记录
  process.once('unhandledRejection', (reason, promise) => {
    console.error('未处理的Promise拒绝:', promise, '原因:', reason);
    // 可在此添加日志服务上报逻辑
  });

  // 原有渲染逻辑保持不变
  return isbot(request.headers.get("user-agent") || "")
    ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext)
    : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext);
}

效果说明

  • 服务端抛出的自定义错误会被Await组件正确捕获,前端可以展示友好错误提示。
  • Node.js不会再触发未处理Promise拒绝导致的崩溃。
  • 全局错误处理仅用于日志记录,不会掩盖其他潜在问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 23:38:21