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

Redux Thunk异常:getDeliveryBoxAsync被拒时handleBoxDeliveryAsync仍为fulfilled

问题:Redux Thunk中父Thunk未捕获子Thunk的Promise拒绝状态

问题场景

使用Redux Thunk时,handleBoxDeliveryAsync作为父Thunk会根据scanMode分发不同的子Thunk(getDeliveryBoxAsync/removeDeliveryBoxAsync)。但当子ThunkgetDeliveryBoxAsync被Promise.reject拒绝时,父ThunkhandleBoxDeliveryAsync却始终处于fulfilled状态,无法触发错误处理逻辑。

相关代码

Slice中的reducer处理:

extraReducers: (builder) => {
    builder
        .addCase(handleBoxDeliveryAsync.fulfilled, (state, action) => {
                if (state.scanMode === "remove") {
                  return;
                }
                const payload = action?.payload?.payload as Data;
                const box = payload.data.resource;
                //If box hasn't COLLECTED status
                if (box.status !== "COLLECTED") {
                  toast.warn("Le bac de collecte n'est pas disponible à la livraison");
                  return;
                }
                //If box hasn't been scanned yet
                if (!state.boxes.find((b) => b.external_id === box.external_id)) {
                  state.boxes.push({ external_id: box.external_id, weight: 0 });
                  toast.success("Le bac a été ajouté");
                  return;
                }
                //If box has already been scanned
                toast.warn("Le bac a déjà été scanné");
              })
...

Thunk定义:

export const handleBoxDeliveryAsync = createAppAsyncThunk(
  "collectDelivery/handleBox",
  async (code: string, store) => {
    try {
      if (store.getState().deliveryCollect.scanMode === "add") {
        return store.dispatch(getDeliveryBoxAsync(code));
      }
      if (store.getState().deliveryCollect.scanMode === "remove") {
        return store.dispatch(removeDeliveryBoxAsync(code));
      }
    } catch (e) {
        return Promise.reject(e); // Explicitly reject the promise if an error occurs.
    }
  }
);

export const getDeliveryBoxAsync = createAppAsyncThunk(
  "collectDelivery/getBox",
  async (code: string) => {
    try {
      return await getBox(code);
    } catch (e) {
      return Promise.reject(e);
    }
  }
);

export const removeDeliveryBoxAsync = createAppAsyncThunk(
  "collectDelivery/removeBox",
  async (code: string, thunk) => {
    if (
      thunk
        .getState()
        .deliveryCollect.boxes.map((b) => b.external_id)
        .includes(code)
    ) {
      return Promise.resolve(code);
    }
    return Promise.reject(code);
  }
);

原因分析

  • store.dispatch()调用Thunk时,始终返回一个已resolved的Promise,无论内部子Thunk是fulfilled还是rejected。它返回的是Thunk执行后生成的action对象,而非子Thunk的Promise结果。因此父Thunk的try/catch无法捕获子Thunk的错误,导致父Thunk始终进入fulfilled状态。
  • 子Thunk中返回Promise.reject(e)只会让子Thunk自身进入rejected状态,但父Thunk拿到的是dispatch返回的resolved结果,不会触发自身的catch块。

解决方案

1. 使用unwrap()提取子Thunk的真实Promise结果

修改handleBoxDeliveryAsync,通过unwrap()方法获取子Thunk的实际Promise状态,子Thunk拒绝时会直接抛出错误,被父Thunk捕获:

export const handleBoxDeliveryAsync = createAppAsyncThunk(
  "collectDelivery/handleBox",
  async (code: string, { getState, dispatch }) => {
    const scanMode = getState().deliveryCollect.scanMode;
    if (scanMode === "add") {
      // unwrap()会提取子Thunk的真实Promise结果,拒绝时抛出错误
      const result = await dispatch(getDeliveryBoxAsync(code)).unwrap();
      return result;
    }
    if (scanMode === "remove") {
      const result = await dispatch(removeDeliveryBoxAsync(code)).unwrap();
      return result;
    }
  }
);

2. 添加父Thunk的rejected状态处理

在slice的extraReducers中新增handleBoxDeliveryAsync.rejected的处理逻辑,响应错误场景:

extraReducers: (builder) => {
    builder
        .addCase(handleBoxDeliveryAsync.fulfilled, (state, action) => {
                // 原有的fulfilled逻辑
                if (state.scanMode === "remove") {
                  return;
                }
                const payload = action?.payload?.payload as Data;
                const box = payload.data.resource;
                if (box.status !== "COLLECTED") {
                  toast.warn("Le bac de collecte n'est pas disponible à la livraison");
                  return;
                }
                if (!state.boxes.find((b) => b.external_id === box.external_id)) {
                  state.boxes.push({ external_id: box.external_id, weight: 0 });
                  toast.success("Le bac a été ajouté");
                  return;
                }
                toast.warn("Le bac a déjà été scanné");
              })
        .addCase(handleBoxDeliveryAsync.rejected, (state, action) => {
                // 处理父Thunk被拒绝的情况
                toast.error("扫描失败:" + (action.error.message || "未知错误"));
              })
...

3. 简化子Thunk的错误处理

createAppAsyncThunk会自动捕获函数内抛出的错误并标记为rejected状态,无需手动返回Promise.reject:

export const getDeliveryBoxAsync = createAppAsyncThunk(
  "collectDelivery/getBox",
  async (code: string) => {
    // 直接返回请求结果,错误会被自动捕获
    return await getBox(code);
  }
);

export const removeDeliveryBoxAsync = createAppAsyncThunk(
  "collectDelivery/removeBox",
  async (code: string, { getState }) => {
    const hasBox = getState().deliveryCollect.boxes.some(b => b.external_id === code);
    if (hasBox) {
      return code;
    }
    // 直接抛出错误,createAppAsyncThunk会处理为rejected状态
    throw new Error("该箱子未被扫描过");
  }
);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 12:51:06