亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

JS 異步錯(cuò)誤捕獲二三事

Java_oldboy / 1286人閱讀

摘要:以及異步代碼在中是特別常見(jiàn)的,我們?cè)撛趺醋霾疟容^無(wú)法捕獲的情況這段代碼中,的回調(diào)函數(shù)拋出一個(gè)錯(cuò)誤,并不會(huì)在中捕獲,會(huì)導(dǎo)致程序直接報(bào)錯(cuò)崩掉。用捕獲異步錯(cuò)誤把異步操作用包裝,通過(guò)內(nèi)部判斷,把錯(cuò)誤,在外面通過(guò)捕獲。

引入

我們都知道 try catch 無(wú)法捕獲 setTimeout 異步任務(wù)中的錯(cuò)誤,那其中的原因是什么。以及異步代碼在 js 中是特別常見(jiàn)的,我們?cè)撛趺醋霾疟容^?

無(wú)法捕獲的情況
function main() {
  try {
    setTimeout(() => {
      throw new Error("async error")
    }, 1000)
  } catch(e) {
    console.log(e, "err")
    console.log("continue...")
  }
}

main();

這段代碼中,setTimeout 的回調(diào)函數(shù)拋出一個(gè)錯(cuò)誤,并不會(huì)在 catch 中捕獲,會(huì)導(dǎo)致程序直接報(bào)錯(cuò)崩掉。

所以說(shuō)在 js 中 try catch 并不是說(shuō)寫上一個(gè)就可以高枕無(wú)憂了。難道每個(gè)函數(shù)都要寫嗎,
那什么情況下 try catch 無(wú)法捕獲 error 呢?

異步任務(wù)

宏任務(wù)的回調(diào)函數(shù)中的錯(cuò)誤無(wú)法捕獲

上面的栗子稍微改一下,主任務(wù)中寫一段 try catch,然后調(diào)用異步任務(wù) task,task 會(huì)在一秒之后拋出一個(gè)錯(cuò)誤。

// 異步任務(wù)
const task = () => {
  setTimeout(() => {
   throw new Error("async error")
 }, 1000)
}
// 主任務(wù)
function main() {
  try {
    task();
  } catch(e) {
    console.log(e, "err")
    console.log("continue...")
  }
}

這種情況下 main 是無(wú)法 catch error 的,這跟瀏覽器的執(zhí)行機(jī)制有關(guān)。異步任務(wù)由 eventloop 加入任務(wù)隊(duì)列,并取出入棧(js 主進(jìn)程)執(zhí)行,而當(dāng) task 取出執(zhí)行的時(shí)候, main 的棧已經(jīng)退出了,也就是上下文環(huán)境已經(jīng)改變,所以 main 無(wú)法捕獲 task 的錯(cuò)誤。

事件回調(diào),請(qǐng)求回調(diào)同屬 tasks,所以道理是一樣的。eventloop 復(fù)習(xí)可以看這篇文章

微任務(wù)(promise)的回調(diào)

// 返回一個(gè) promise 對(duì)象
const promiseFetch = () => 
  new Promise((reslove) => {
  reslove();
})

function main() {
  try {
    // 回調(diào)函數(shù)里拋出錯(cuò)誤
    promiseFetch().then(() => {
      throw new Error("err")
    })
  } catch(e) {
    console.log(e, "eeee");
    console.log("continue");
  }
}

promise 的任務(wù),也就是 then 里面的回調(diào)函數(shù),拋出錯(cuò)誤同樣也無(wú)法 catch。因?yàn)槲⑷蝿?wù)隊(duì)列是在兩個(gè) task 之間清空的,所以 then 入棧的時(shí)候,main 函數(shù)也已經(jīng)出棧了。

并不是回調(diào)函數(shù)無(wú)法 try catch

很多人可能有一個(gè)誤解,因?yàn)榇蟛糠钟龅綗o(wú)法 catch 的情況,都發(fā)生在回調(diào)函數(shù),就認(rèn)為回調(diào)函數(shù)不能 catch。

不全對(duì),看一個(gè)最普通的栗子。

// 定義一個(gè) fn,參數(shù)是函數(shù)。
const fn = (cb: () => void) => {
  cb();
};

function main() {
  try {
    // 傳入 callback,fn 執(zhí)行會(huì)調(diào)用,并拋出錯(cuò)誤。
    fn(() => {
      throw new Error("123");
    })
  } catch(e) {
    console.log("error");
  }
}
main();

結(jié)果當(dāng)然是可以 catch 的。因?yàn)?callback 執(zhí)行的時(shí)候,跟 main 還在同一次事件循環(huán)中,即一個(gè) eventloop tick。所以上下文沒(méi)有變化,錯(cuò)誤是可以 catch 的。
根本原因還是同步代碼,并沒(méi)有遇到異步任務(wù)。

promise 的異常捕獲 構(gòu)造函數(shù)

先看兩段代碼:

function main1() {
  try {
    new Promise(() => {
      throw new Error("promise1 error")
    })
  } catch(e) {
    console.log(e.message);
  }
}

function main2() {
  try {
    Promise.reject("promise2 error");
  } catch(e) {
    console.log(e.message);
  }
}

以上兩個(gè) try catch 都不能捕獲到 error,因?yàn)?promise 內(nèi)部的錯(cuò)誤不會(huì)冒泡出來(lái),而是被 promise 吃掉了,只有通過(guò) promise.catch 才可以捕獲,所以用 Promise 一定要寫 catch 啊。

然后我們?cè)賮?lái)看一下使用 promise.catch 的兩段代碼:

// reject
const p1 = new Promise((reslove, reject) => {
  if(1) {
    reject();
  }
});
p1.catch((e) => console.log("p1 error"));
// throw new Error
const p2 = new Promise((reslove, reject) => {
  if(1) {
    throw new Error("p2 error")
  }
});

p2.catch((e) => console.log("p2 error"));

promise 內(nèi)部的無(wú)論是 reject 或者 throw new Error,都可以通過(guò) catch 回調(diào)捕獲。

這里要跟我們最開(kāi)始微任務(wù)的栗子區(qū)分,promise 的微任務(wù)指的是 then 的回調(diào),而此處是 Promise 構(gòu)造函數(shù)傳入的第一個(gè)參數(shù),new Promise 是同步執(zhí)行的。

then

那 then 之后的錯(cuò)誤如何捕獲呢。

function main3() {
  Promise.resolve(true).then(() => {
    try {
      throw new Error("then");
    } catch(e) {
      return e;
    }
  }).then(e => console.log(e.message));
}

只能是在回調(diào)函數(shù)內(nèi)部 catch 錯(cuò)誤,并把錯(cuò)誤信息返回,error 會(huì)傳遞到下一個(gè) then 的回調(diào)。

用 Promise 捕獲異步錯(cuò)誤
const p3 = () =>  new Promise((reslove, reject) => {
  setTimeout(() => {
    reject("async error");
  })
});

function main3() {
  p3().catch(e => console.log(e));
}
main3();

把異步操作用 Promise 包裝,通過(guò)內(nèi)部判斷,把錯(cuò)誤 reject,在外面通過(guò) promise.catch 捕獲。

async/await 的異常捕獲

首先我們模擬一個(gè)請(qǐng)求失敗的函數(shù) fetchFailure,fetch 函數(shù)通常都是返回一個(gè) promise。

main 函數(shù)改成 async,catch 去捕獲 fetchFailure reject 拋出的錯(cuò)誤。能不能獲取到呢。

const fetchFailure = () => new Promise((resolve, reject) => {
  setTimeout(() => {// 模擬請(qǐng)求
    if(1) reject("fetch failure...");
  })
})

async function main () {
  try {
    const res = await fetchFailure();
    console.log(res, "res");
  } catch(e) {
    console.log(e, "e.message");
  }
}
main();

async 函數(shù)會(huì)被編譯成好幾段,根據(jù) await 關(guān)鍵字,以及 catch 等,比如 main 函數(shù)就是拆成三段。

fetchFailure 2. console.log(res) 3. catch

通過(guò) step 來(lái)控制迭代的進(jìn)度,比如 "next",就是往下走一次,從 1->2,異步是通過(guò) Promise.then() 控制的,你可以理解為就是一個(gè) Promise 鏈,感興趣的可以去研究一下。 關(guān)鍵是生成器也有一個(gè) "throw" 的狀態(tài),當(dāng) Promise 的狀態(tài) reject 后,會(huì)向上冒泡,直到 step("throw") 執(zhí)行,然后 catch 里的代碼 console.log(e, "e.message"); 執(zhí)行。

明顯感覺(jué) async/await 的錯(cuò)誤處理更優(yōu)雅一些,當(dāng)然也是內(nèi)部配合使用了 Promise。

更進(jìn)一步

async 函數(shù)處理異步流程是利器,但是它也不會(huì)自動(dòng)去 catch 錯(cuò)誤,需要我們自己寫 try catch,如果每個(gè)函數(shù)都寫一個(gè),也挺麻煩的,比較業(yè)務(wù)中異步函數(shù)會(huì)很多。

首先想到的是把 try catch,以及 catch 后的邏輯抽取出來(lái)。

const handle = async (fn: any) => {
  try {
    return await fn();
  } catch(e) {
    // do sth
    console.log(e, "e.messagee");
  }
}

async function main () {
    const res = await handle(fetchFailure);
    console.log(res, "res");
}

寫一個(gè)高階函數(shù)包裹 fetchFailure,高階函數(shù)復(fù)用邏輯,比如此處的 try catch,然后執(zhí)行傳入的參數(shù)-函數(shù) 即可。

然后,加上回調(diào)函數(shù)的參數(shù)傳遞,以及返回值遵守 first-error,向 node/go 的語(yǔ)法看齊。如下:

const handleTryCatch = (fn: (...args: any[]) => Promise<{}>) => async (...args: any[]) => {
  try {
    return [null, await fn(...args)];
  } catch(e) {
    console.log(e, "e.messagee");
    return [e];
  }
}

async function main () {
  const [err, res] = await handleTryCatch(fetchFailure)("");
  if(err) {
    console.log(err, "err");
    return;
  }
  console.log(res, "res");
}

但是還有幾個(gè)問(wèn)題,一個(gè)是 catch 后的邏輯,這塊還不支持自定義,再就是返回值總要判斷一下,是否有 error,也可以抽象一下。
所以我們可以在高階函數(shù)的 catch 處做一下文章,比如加入一些錯(cuò)誤處理的回調(diào)函數(shù)支持不同的邏輯,然后一個(gè)項(xiàng)目中錯(cuò)誤處理可以簡(jiǎn)單分幾類,做不同的處理,就可以盡可能的復(fù)用代碼了。

// 1. 三階函數(shù)。第一次傳入錯(cuò)誤處理的 handle,第二次是傳入要修飾的 async 函數(shù),最后返回一個(gè)新的 function。

const handleTryCatch = (handle: (e: Error) => void = errorHandle) =>
  (fn: (...args: any[]) => Promise<{}>) => async(...args: any[]) => {
    try {
      return [null, await fn(...args)];
    } catch(e) {
      return [handle(e)];
    }
  }
   
// 2. 定義各種各樣的錯(cuò)誤類型
//    我們可以把錯(cuò)誤信息格式化,成為代碼里可以處理的樣式,比如包含錯(cuò)誤碼和錯(cuò)誤信息
class DbError extends Error {
  public errmsg: string;
  public errno: number;
  constructor(msg: string, code: number) {
    super(msg);
    this.errmsg = msg || "db_error_msg";
    this.errno = code || 20010;
  }
}
class ValidatedError extends Error {
  public errmsg: string;
  public errno: number;
  constructor(msg: string, code: number) {
    super(msg);
    this.errmsg = msg || "validated_error_msg";
    this.errno = code || 20010;
  }
}

// 3. 錯(cuò)誤處理的邏輯,這可能只是其中一類。通常錯(cuò)誤處理都是按功能需求來(lái)劃分
//    比如請(qǐng)求失?。?00 但是返回值有錯(cuò)誤信息),比如 node 中寫 db 失敗等。
const errorHandle = (e: Error) => {
  // do something
  if(e instanceof ValidatedError || e instanceof DbError) {
    // do sth
    return e;
  }
  return {
    code: 101,
    errmsg: "unKnown"
  };
}   
const usualHandleTryCatch = handleTryCatch(errorHandle);

// 以上的代碼都是多個(gè)模塊復(fù)用的,那實(shí)際的業(yè)務(wù)代碼可能只需要這樣。
async function main () {
  const [error, res] = await usualHandleTryCatch(fetchFail)(false);
  if(error) {
    // 因?yàn)?catch 已經(jīng)做了攔截,甚至可以加入一些通用邏輯,這里甚至不用判斷 if error
    console.log(error, "error");
    return;
  }
  console.log(res, "res");
}

解決了一些錯(cuò)誤邏輯的復(fù)用問(wèn)題之后,即封裝成不同的錯(cuò)誤處理器即可。但是這些處理器在使用的時(shí)候,因?yàn)槎际歉唠A函數(shù),可以使用 es6 的裝飾器寫法。

不過(guò)裝飾器只能用于類和類的方法,所以如果是函數(shù)的形式,就不能使用了。不過(guò)在日常開(kāi)發(fā)中,比如 React 的組件,或者 Mobx 的 store,都是以 class 的形式存在的,所以使用場(chǎng)景挺多的。

比如改成類裝飾器:

const asyncErrorWrapper = (errorHandler: (e: Error) => void = errorHandle) => (target: Function) => {
  const props = Object.getOwnPropertyNames(target.prototype);
  props.forEach((prop) => {
      var value = target.prototype[prop];
      if(Object.prototype.toString.call(value) === "[object AsyncFunction]"){
        target.prototype[prop] = async (...args: any[]) => {
          try{
            return await value.apply(this,args);
          }catch(err){
            return errorHandler(err);
          }
        }
      }
  });
}

@asyncErrorWrapper(errorHandle)
class Store {
  async getList (){
    return Promise.reject("類裝飾:失敗了");
  }
}

const store = new Store();

async function main() {
  const o = await store.getList();
}
main();

這種 class 裝飾器的寫法是看到黃子毅 這么寫過(guò),感謝靈感。

koa 的錯(cuò)誤處理

如果對(duì) koa 不熟悉,可以選擇跳過(guò)不看。

koa 中當(dāng)然也可以用上面 async 的做法,不過(guò)通常我們用 koa 寫 server 的時(shí)候,都是處理請(qǐng)求,一次 http 事務(wù)會(huì)掉起響應(yīng)的中間件,所以 koa 的錯(cuò)誤處理很好的利用了中間件的特性。

比如我的做法是,第一個(gè)中間件為捕獲 error,因?yàn)檠笫[模型的緣故,第一個(gè)中間件最后仍會(huì)執(zhí)行,而當(dāng)某個(gè)中間件拋出錯(cuò)誤后,我期待能在此捕獲并處理。

// 第一個(gè)中間件
const errorCatch = async(ctx, next) => {
  try {
    await next();
  } catch(e) {
    // 在此捕獲 error 路由,throw 出的 Error
    console.log(e, e.message, "error");
    ctx.body = "error";
  }
}

app.use(errorCatch);

// logger
app.use(async (ctx, next) => {
  console.log(ctx.req.body, "body");
  await next();
})

// router 的某個(gè)中間件
router.get("/error", async (ctx, next) => {
  if(1) {
    throw new Error("錯(cuò)誤測(cè)試")
  }
  await next();
})

為什么在第一個(gè)中間件寫上 try catch,就可以捕獲前面中間件 throw 出的錯(cuò)誤呢。首先我們前面 async/await 的地方解釋過(guò),async 中await handle(),handle 函數(shù)內(nèi)部的 throw new Error 或者 Promise.reject() 是可以被 async 的 catch 捕獲的。所以只需要 next 函數(shù)能夠拿到錯(cuò)誤,并拋出就可以了,那看看 next 函數(shù)。

// compose 是傳入中間件的數(shù)組,最終形成中間件鏈的,next 控制游標(biāo)。
 compose(middlewares) {
    return (context) => {
      let index = 0;
      // 為了每個(gè)中間件都可以是異步調(diào)用,即 `await next()` 這種寫法,每個(gè) next 都要返回一個(gè) promise 對(duì)象

      function next(index) {
        const func = middlewares[index];
        try {
          // 在此處寫 try catch,因?yàn)槭菍懙?Promise 構(gòu)造體中的,所以拋出的錯(cuò)誤能被 catch
          return new Promise((resolve, reject) => {
            if (index >= middlewares.length) return reject("next is inexistence");
            resolve(func(context, () => next(index + 1)));
          });
        } catch(err) {
          // 捕獲到錯(cuò)誤,返回錯(cuò)誤
          return Promise.reject(err);
        }
      }
      return next(index);
    }
  }

next 函數(shù)根據(jù) index,取出當(dāng)前的中間件執(zhí)行。中間件函數(shù)如果是 async 函數(shù),同樣的轉(zhuǎn)化為 generator 執(zhí)行,內(nèi)部的異步代碼順序由它自己控制,而我們知道 async 函數(shù)的錯(cuò)誤是可以通過(guò) try catch 捕獲的,所以在 next 函數(shù)中加上 try catch 捕獲中間件函數(shù)的錯(cuò)誤,再 return 拋出去即可。所以我們才可以在第一個(gè)中間件捕獲。詳細(xì)代碼可以看下簡(jiǎn)版 koa

然后 koa 還提供了 ctx.throw 和全局的 app.on 來(lái)捕獲錯(cuò)誤。
如果你沒(méi)有寫錯(cuò)誤處理的中間件,那可以使用 ctx.throw 返回前端,不至于讓代碼錯(cuò)誤。
但是 throw new Error 也是有優(yōu)勢(shì)的,因?yàn)槟硞€(gè)中間件的代碼邏輯中,一旦出現(xiàn)我們不想讓后面的中間件執(zhí)行,直接給前端返回,直接拋出錯(cuò)誤即可,讓通用的中間件處理,反正都是錯(cuò)誤信息。

// 定義不同的錯(cuò)誤類型,在此可以捕獲,并處理。
const errorCatch = async(ctx, next) => {
  try {
    await next();
 } catch (err) {
    const { errmsg, errno, status = 500, redirect } = err;
    
    if (err instanceof ValidatedError || err instanceof DbError || err instanceof AuthError || err instanceof RequestError) {
      ctx.status = 200;
      ctx.body = {
        errmsg,
        errno,
      };
      return;
    }
    ctx.status = status;
    if (status === 302 && redirect) {
      console.log(redirect);
      ctx.redirect(redirect);
    }
    if (status === 500) {
      ctx.body = {
        errmsg: err.message,
        errno: 90001,
      };
      ctx.app.emit("error", err, ctx);
    }
  }
}

app.use(errorCatch);

// logger
app.use(async (ctx, next) => {
  console.log(ctx.req.body, "body");
  await next();
})

// 通過(guò) ctx.throw
app.use(async (ctx, next) => {
  //will NOT log the error and will return `Error Message` as the response body with status 400
  ctx.throw(400,"Error Message");
}); 

// router 的某個(gè)中間件
router.get("/error", async (ctx, next) => {
  if(1) {
    throw new Error("錯(cuò)誤測(cè)試")
  }
  await next();
})

// 最后的兜底
app.on("error", (err, ctx) => {
  /* centralized error handling:
   *   console.log error
   *   write error to log file
   *   save error and request information to database if ctx.request match condition
   *   ...
  */
});
最后

本文的代碼都存放于此

總的來(lái)說(shuō),目前 async 結(jié)合 promise 去處理 js 的異步錯(cuò)誤會(huì)是比較方便的。另外,成熟的框架(react、koa)對(duì)于錯(cuò)誤處理都有不錯(cuò)的方式,盡可能去看一下官方是如何處理的。

這只是我對(duì) js 中處理異步錯(cuò)誤的一些理解。不過(guò)前端的需要捕獲異常的地方有很多,比如前端的代碼錯(cuò)誤,cors 跨域錯(cuò)誤,iframe 的錯(cuò)誤,甚至 react 和 vue 的錯(cuò)誤我們都需要處理,以及異常的監(jiān)控和上報(bào),以幫助我們及時(shí)的解決問(wèn)題以及分析穩(wěn)定性。采取多種方案應(yīng)用到我們的項(xiàng)目中,讓我們不擔(dān)心頁(yè)面掛了,或者又報(bào) bug 了,才能安安穩(wěn)穩(wěn)的去度假休息

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/103836.html

相關(guān)文章

  • 與dom事件流相關(guān)的三事

    摘要:但對(duì)于整個(gè)事件流上的別的元素來(lái)說(shuō),執(zhí)行順序還會(huì)受到另外一個(gè)因素的影響。以上面的場(chǎng)景為例,在捕獲階段執(zhí)行的事件,如果執(zhí)行,則事件流終止,不會(huì)到達(dá)目標(biāo)階段,的世界則不會(huì)被執(zhí)行執(zhí)行結(jié)果為線上參考事件流 向dom綁定事件的事件的三種方式 行內(nèi)綁定 按鈕 js內(nèi)綁定 btnDom.onclick = function clickHandler() { console.log(click)...

    Anleb 評(píng)論0 收藏0
  • 前端渲染過(guò)程的三事

    摘要:前端渲染過(guò)程的二三事本文不會(huì)介紹整個(gè)前端渲染過(guò)程的步驟,只是記錄最近閱讀的文章的些許思考和感悟。那么現(xiàn)在我們可以明白這個(gè)問(wèn)題的關(guān)鍵所在了,因?yàn)樵诖蟛糠猪?yè)面中是擁有的,而由于其解析順序,那么在事件之前必定已經(jīng)成功構(gòu)造樹(shù)。 前端渲染過(guò)程的二三事 本文不會(huì)介紹整個(gè)前端渲染過(guò)程的步驟,只是記錄最近閱讀的文章的些許思考和感悟。(文章地址一(系列),文章地址二) 希望大家在閱讀這篇文章之前能將上述...

    Rindia 評(píng)論0 收藏0
  • 文件上傳三事

    摘要:選擇文件談到文件上傳,不得不提,中文名叫表單。當(dāng)需要使用回調(diào)函數(shù)來(lái)處理上傳完成后后端返回的數(shù)據(jù)時(shí),需要和后端預(yù)先達(dá)成約定,如,回調(diào)函數(shù)名,參數(shù)列表,等等。后端可以根據(jù)邊界的檢驗(yàn),識(shí)別上傳的文件,讀取元數(shù)據(jù)中的文件屬性,從而為驗(yàn)證提供數(shù)據(jù)。 引子 其實(shí)很早就開(kāi)始醞釀這一篇了,無(wú)奈總是發(fā)現(xiàn)有缺漏的地方,遂努力惡補(bǔ)前端+后端+底層相關(guān)知識(shí)。今天終于可以發(fā)表了。 --跟生孩子一樣啊。 選擇文...

    wudengzan 評(píng)論0 收藏0
  • 應(yīng)用開(kāi)發(fā)者必須了解的Kubernetes網(wǎng)絡(luò)三事

    摘要:在容器領(lǐng)域內(nèi),已毋庸置疑成為了容器編排和管理的社區(qū)標(biāo)準(zhǔn)??蛻舳藷o(wú)需連接到每個(gè)的,而是直接連接負(fù)載均衡器的地址。通過(guò)這樣的操作,使用持續(xù)交付和部署方法論的快速開(kāi)發(fā)和部署周期將會(huì)成為常態(tài)。 在容器領(lǐng)域內(nèi),Kubernetes已毋庸置疑成為了容器編排和管理的社區(qū)標(biāo)準(zhǔn)。如果你希望你所搭建的應(yīng)用程序能充分利用多云(multi-cloud)的優(yōu)勢(shì),有一些與Kubernetes網(wǎng)絡(luò)相關(guān)的基本內(nèi)容是你...

    劉明 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<