import get from 'lodash-es/get.js'
import isbol from './isbol.mjs'
import ispint from './ispint.mjs'
import isestr from './isestr.mjs'
import cint from './cint.mjs'
//本函數刻意不使用isErr、isstr、isobj、isarr等型別守衛做分派
// 原因: 該四者皆建立於Object.prototype.toString.call與lodash isError之上, 而
// (1) Object.prototype.toString.call會觸發Symbol.toStringTag之getter
// (2) lodash isError第三子句會讀取value.message
// (3) haskey之key in obj會觸發Proxy之has陷阱
// 三者皆可拋錯, 且拋在任何try之前, 故「先判型別再分支」之架構無法以逐點包try補完
// 改採候選階梯: 不做型別分派, 依序嘗試各候選來源, 每階獨立try, 取第一個非空字串
//prim, 值轉字串, 只處理證實不觸發使用者程式碼者, 其餘回''交由後段處理
// typeof對任何值(含已撤銷之Proxy)皆不拋錯, 是唯一絕對安全之型別探測
function prim(v) {
let t = typeof v
if (t === 'string') {
return v
}
if (v === null || v === undefined) {
return ''
}
if (t === 'number' || t === 'boolean' || t === 'bigint') {
return String(v)
}
if (t === 'symbol') {
return String(v) //Symbol僅String()可轉, 樣板字面量與+皆拋TypeError
}
return ''
}
//attempt, 執行取值函數並保證回傳字串, 任何拋錯或非字串一律回''
function attempt(fn) {
try {
let v = fn()
return (typeof v === 'string') ? v : ''
}
catch (err) {
return ''
}
}
//jsonSafe, 序列化並保證回傳字串, 去環、轉BigInt、限深度, 全程不拋錯
// 不用o2j之理由: o2j之契約為「序列化失敗回空字串」, 對循環參照與BigInt一律回''
// 本函數需要的是盡力降級(環處填[Circular]、BigInt轉字串、超深填[Deep]), 若改動o2j
// 將變更其既有契約而波及其呼叫端, 故於此另行實作
function jsonSafe(v, levelMax) {
try {
let seen = new WeakSet()
let stack = []
let r = JSON.stringify(v, function(k, val) {
while (stack.length > 0 && stack[stack.length - 1] !== this) {
stack.pop()
}
if (stack.length >= levelMax) {
return '[Deep]'
}
if (typeof val === 'bigint') {
return String(val)
}
if (val !== null && typeof val === 'object') {
if (seen.has(val)) {
return '[Circular]'
}
seen.add(val)
stack.push(val)
}
return val
})
//JSON.stringify對undefined、Symbol、函數、以及toJSON回undefined者
//會「正常回傳undefined」而非拋錯, 故不可只靠try catch, 須驗回傳型別
return (typeof r === 'string') ? r : ''
}
catch (err) {
return ''
}
}
//unquote, toJSON回字串者(如Date)之JSON輸出會多包一層引號, 剝掉才是可讀值
function unquote(j) {
if (j.length >= 2 && j[0] === '"' && j[j.length - 1] === '"') {
let u = attempt(() => {
return JSON.parse(j)
})
if (u !== '') {
return u
}
}
return j
}
//normalizeMsg, message為非字串時轉為字串
function normalizeMsg(raw, levelMax) {
if (raw === undefined) {
return '' //視為無message, 由呼叫端往下一階
}
if (raw === null) {
return 'null'
}
let p = prim(raw)
if (p !== '') {
return p
}
let j = jsonSafe(raw, levelMax)
if (j !== '' && j !== '{}') {
return unquote(j)
}
let s = attempt(() => {
return String(raw)
})
if (s !== '' && s !== '[object Object]') {
return s
}
return j
}
//readCause, 沿cause鏈取根因
// WeakSet與深度上限兩者皆需: WeakSet擋環(自我參照、互相參照), 深度上限擋合法之超長鏈
function readCause(err, o) {
let seen = new WeakSet()
if (err !== null && (typeof err === 'object' || typeof err === 'function')) {
seen.add(err) //起點須先入集合, 否則a->b->a會把a再展開一次
}
let cur = err
let ms = []
for (let i = 0; i < o.levelCauseMax; i++) {
let nx = null
try {
nx = cur.cause
}
catch (_err) {
break
}
if (nx === null || nx === undefined) {
break
}
if (typeof nx === 'object' || typeof nx === 'function') {
if (seen.has(nx)) {
break
}
seen.add(nx)
}
let m = core(nx, { ...o, useCause: false, useAggregate: false })
if (m !== '') {
ms.push(m)
}
cur = nx
}
return ms.join(' <- ')
}
//readAggregate, 取AggregateError之errors各項message
function readAggregate(err, o) {
let arr = null
try {
arr = err.errors
}
catch (_err) {
return ''
}
if (!Array.isArray(arr) || arr.length === 0) {
return ''
}
let ms = []
for (let i = 0; i < arr.length && i < o.levelCauseMax; i++) {
if (arr[i] === err) {
continue //errors含自身者跳過, 避免重複展開
}
let m = core(arr[i], { ...o, useCause: false, useAggregate: false })
if (m !== '') {
ms.push(m)
}
}
return ms.join('; ')
}
//core, 候選階梯本體
function core(err, o) {
//階0, typeof快篩, 對原始型別直接回, 零成本且絕對安全, 涵蓋最常見之throw 'msg'
if (typeof err === 'string') {
return err //含空字串, 原樣回傳
}
if (err === null || err === undefined) {
return ''
}
if (typeof err === 'number') {
return Number.isNaN(err) ? '' : String(err)
}
if (typeof err === 'boolean' || typeof err === 'bigint' || typeof err === 'symbol') {
return String(err)
}
//以下err必為object或function
//階1, 取message, 全程只讀一次
let raw = null
let bMsg = false
try {
raw = err.message
bMsg = true
}
catch (_err) {
bMsg = false
}
if (bMsg && typeof raw === 'string') {
//message為非空字串, 直接為結果
if (raw !== '') {
let head = raw
if (o.useCause) {
let c = readCause(err, o)
if (c !== '') {
head = `${head} <- ${c}`
}
}
if (o.useAggregate) {
let a = readAggregate(err, o)
if (a !== '') {
head = `${head} [${a}]`
}
}
return head
}
//message為空字串, 屬「有message欄位但無內容」, 沿用既有語意回空字串
// 惟若有cause則取之, 因該情形下呼叫端與本函數皆取不到任何資訊, 而資訊確實存在
let c = readCause(err, o)
if (c !== '') {
return c
}
return ''
}
//message非字串者先正規化
if (bMsg) {
let m = normalizeMsg(raw, o.levelJsonMax)
if (m !== '') {
return m
}
}
//階2, cause與aggregate, 用於無message但有此二者之情形
let c = readCause(err, o)
if (c !== '') {
return c
}
if (o.useAggregate) {
let a = readAggregate(err, o)
if (a !== '') {
return a
}
}
//階3, JSON序列化
let j = jsonSafe(err, o.levelJsonMax)
let ju = unquote(j)
if (ju !== '' && ju !== '{}') {
return ju
}
//階4, String(err), 救回Date、RegExp、函數、Map、宿主物件等JSON給不出內容者
let s = attempt(() => {
return String(err)
})
if (s !== '' && s !== '[object Object]') {
return s
}
//階5, 型別標籤, 本身亦會觸發Symbol.toStringTag之getter故包try
let t = attempt(() => {
return Object.prototype.toString.call(err)
})
if (t !== '' && t !== '[object Object]') {
return t
}
//階6, 序列化結果縱為'{}'仍優於空字串
if (j !== '') {
return j
}
return ''
}
/**
* 提取Error內Message訊息
* err接受任意輸入,於任何err下皆不拋出錯誤且回傳值必為字串
* 此不拋錯之保證不涵蓋opt,因opt係呼叫端自備之設定物件,其讀取比照本套件慣例不另包try
*
* Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/getErrorMessage.test.mjs Github}
* @memberOf wsemi
* @param {*} err 傳入任意錯誤資料
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Boolean} [opt.useCause=false] 輸入是否串接cause鏈布林值,若為true則將各層cause之訊息以' <- '串接,預設false。惟外層message為空字串時,不論本項為何皆取cause,因該情形下無其他資訊來源
* @param {Boolean} [opt.useAggregate=false] 輸入是否附加AggregateError之errors各項訊息布林值,若為true則以'; '串接後置於方括號內,預設false
* @param {Integer} [opt.levelCauseMax=8] 輸入cause鏈與errors之最大展開層數正整數,預設8
* @param {Integer} [opt.levelJsonMax=6] 輸入序列化之最大巢狀深度正整數,超過者以'[Deep]'表示,預設6
* @param {Integer} [opt.lengthMessageMax=0] 輸入回傳字串之最大長度正整數,超過者截斷並以'...'標記,該標記計入本上限內,預設0代表不限制長度
* @returns {String} 回傳錯誤訊息字串。無法提取時回傳空字串。序列化時循環參照處以'[Circular]'表示、BigInt轉為字串,此二標記不做跳脫,應用資料內若恰有相同字串將無法分辨
* @example
*
* try {
* throw new Error('something wrong')
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => something wrong
*
* try {
* throw new Error()
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => ''
*
* try {
* throw new TypeError('wrong type')
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => wrong type
*
* try {
* throw new RangeError('range bad')
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => range bad
*
* try {
* throw new ReferenceError('ref bad')
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => ref bad
*
* try {
* throw new SyntaxError('syntax bad')
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => syntax bad
*
* try {
* throw new URIError('uri bad')
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => uri bad
*
* try {
* throw new AggregateError([new Error('e1'), 'e2'], 'outer')
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => outer
*
* try {
* throw new AggregateError([new Error('e1'), new Error('e2')], 'outer')
* }
* catch (err) {
* console.log(getErrorMessage(err, { useAggregate: true }))
* }
* // => outer [e1; e2]
*
* try {
* throw new Error('top', { cause: new Error('root cause') })
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => top
*
* try {
* throw new Error('top', { cause: new Error('root cause') })
* }
* catch (err) {
* console.log(getErrorMessage(err, { useCause: true }))
* }
* // => top <- root cause
*
* try {
* throw new Error('', { cause: new Error('root only') })
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => root only
*
* try {
* throw new DOMException('operation was aborted.', 'AbortError')
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => operation was aborted.
*
* try {
* fs.readFileSync('definitely_not_exists_1234567890.txt')
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => ENOENT: no such file or directory, open ...
*
* let o = { a: 1 }
* o.self = o
* console.log(getErrorMessage(o))
* // => {"a":1,"self":"[Circular]"}
*
* console.log(getErrorMessage({ v: 10n }))
* // => {"v":"10"}
*
* console.log(getErrorMessage({ message: 123 }))
* // => 123
*
* console.log(getErrorMessage(/ab/g))
* // => /ab/g
*
* let test1 = async() => {
* return Promise.reject('promise reject')
* }
* try {
* await test1()
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => promise reject
*
* let test2 = async() => {
* throw new Error('something wrong')
* }
* try {
* await test2()
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => something wrong
*
* let test3 = async() => {
* throw new Error()
* }
* try {
* await test3()
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => ''
*
* let test4 = async() => {
* throw new TypeError('wrong type')
* }
* try {
* await test4()
* }
* catch (err) {
* console.log(getErrorMessage(err))
* }
* // => wrong type
*
*/
function getErrorMessage(err, opt = {}) {
//useCause
let useCause = get(opt, 'useCause', null)
if (!isbol(useCause)) {
useCause = false
}
//useAggregate
let useAggregate = get(opt, 'useAggregate', null)
if (!isbol(useAggregate)) {
useAggregate = false
}
//levelCauseMax
let levelCauseMax = get(opt, 'levelCauseMax', null)
if (!ispint(levelCauseMax, { useLimitSafe: true })) {
levelCauseMax = 8
}
levelCauseMax = cint(levelCauseMax)
//levelJsonMax
let levelJsonMax = get(opt, 'levelJsonMax', null)
if (!ispint(levelJsonMax, { useLimitSafe: true })) {
levelJsonMax = 6
}
levelJsonMax = cint(levelJsonMax)
//lengthMessageMax
let lengthMessageMax = get(opt, 'lengthMessageMax', null)
if (!ispint(lengthMessageMax, { useLimitSafe: true })) {
lengthMessageMax = 0
}
lengthMessageMax = cint(lengthMessageMax)
//optCore
let optCore = {
useCause,
useAggregate,
levelCauseMax,
levelJsonMax,
lengthMessageMax,
}
//r, 整體try, 任何未預期之拋錯皆有底, 使本函數於任何輸入下皆不拋錯
let r = ''
try {
r = core(err, optCore)
}
catch (_err) {
r = ''
return r
}
//出口正規化, 保證回傳字串
if (!isestr(r)) {
r = ''
return r
}
//截斷, 使回傳長度不超過lengthMessageMax, 避免超長訊息塞爆日誌
// '...'標記計入上限內, 故回傳長度必然小於等於lengthMessageMax; 上限不大於3時無空間置放標記故直接截斷
if (optCore.lengthMessageMax > 0 && r.length > optCore.lengthMessageMax) {
if (optCore.lengthMessageMax > 3) {
r = r.slice(0, optCore.lengthMessageMax - 3) + '...'
}
else {
r = r.slice(0, optCore.lengthMessageMax)
}
}
return r
}
export default getErrorMessage