Last active
July 4, 2025 11:30
-
-
Save petsel/7f7e5b56f2bde78698474d0ad0b1fd4f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const defaultDescriptorOptions = { enumerable: false, writable: true, configurable: true }; | |
const sealedDescriptorOptions = { enumerable: false, configurable: false }; | |
/** | |
* Returns the internal `[[Class]]` tag of a value, like `[object String]` or returns `undefined` | |
* if no argument was passed. | |
* | |
* @param {...any} args | |
* A variadic argument list. The first argument (`args[0]`) is the optional `value` parameter. | |
* Its **presence** is detected via `args.length`, allowing the function to distinguish between | |
* an explicitly passed `undefined` value and a completely omitted argument. | |
* @returns {string | undefined} | |
* The value’s internal type signature, or the `undefined` value if no argument was passed. | |
*/ | |
function getTypeSignature(...args) { | |
/** @type {any} */ | |
const value = args[0]; | |
return (args.length >= 1 && Object.prototype.toString.call(value)) || value; | |
} | |
/** | |
* Determines if a value is a function with standard `call`/`apply`/`bind` methods. | |
* | |
* @param {any} [value] | |
* An optionally passed value of any type. | |
* @returns {value is Function} | |
* A boolean value which indicates whether the tested type is any kind of function. | |
*/ | |
function isFunction(value) { | |
return ( | |
'function' === typeof value && | |
'function' === typeof value.bind && | |
'function' === typeof value.call && | |
'function' === typeof value.apply | |
); | |
} | |
/** | |
* Determines if a value is a native async function. | |
* | |
* @param {any} [value] | |
* An optionally passed value of any type. | |
* @returns {value is (...args: any[]) => Promise<any>} | |
* A boolean value which indicates whether the tested value is a native async function. | |
*/ | |
function isAsyncFunction(value) { | |
return getTypeSignature(value) === '[object AsyncFunction]'; | |
} | |
/** | |
* @param {any} [value] | |
* An optionally passed value of any type. | |
* @returns {value is Promise} | |
* A boolean value which indicates whether the tested value is a promise. | |
*/ | |
function isPromise(value) { | |
return getTypeSignature(value) === '[object Promise]'; | |
} | |
/** | |
* @param {any} [value] | |
* An optionally passed value of any type. | |
* @returns {value is string} | |
* A boolean value which indicates whether the tested value is a string-type. | |
*/ | |
function isString(value) { | |
return getTypeSignature(value) === '[object String]'; | |
} | |
/** | |
* Every `safe` method handles the throwing of its operated function | |
* silently. Not every caught exception is an error type. In fact the | |
* exception or the throwing cause can be of any type. | |
* The custom error-type `SafeResultError` wraps any caught non-error | |
* exception into a distinct error instance which does refer the caught | |
* exception by its `cause` property. | |
* | |
* @extends {Error} | |
*/ | |
class SafeResultError extends Error { | |
/** | |
* Does construct a new custom `SafeResultError` instance. | |
* | |
* @param {any} cause | |
* The passed error-cause value which can be of any type. | |
*/ | |
constructor(cause) { | |
// const message = isString(cause) | |
// ? (!cause.trim() && 'Caught with a nonsense reason value.') || 'Caught with a reason.' | |
// : (!cause && 'Caught with a falsy exception.') || 'Caught with a non-error exception.' | |
const message = isString(cause) | |
? cause.trim() || 'Caught with an empty or blank string value.' | |
: (!cause && 'Caught with a falsy exception.') || 'Caught with a non-error exception.'; | |
// re-use `Error::cause`. | |
super(message, { cause }); | |
} | |
// shadow the `name` value. | |
get name() { | |
return 'SafeResultError'; | |
} | |
} | |
// - Seales `SafeResultError` specific constructor properties in order to | |
// achieve a stable type-identity; the base of a reliable type-detection. | |
Object.defineProperty(SafeResultError.prototype, Symbol.toStringTag, { | |
get() { return 'SafeResultError'; }, ...sealedDescriptorOptions, | |
}); | |
Object.defineProperty(SafeResultError.prototype, 'constructor', { | |
value: SafeResultError, ...sealedDescriptorOptions, writable: false, | |
}); | |
Object.defineProperty(SafeResultError, 'name', { | |
value: 'SafeResultError', ...sealedDescriptorOptions, writable: false, | |
}); | |
/** | |
* @param {any} [value] | |
* An optionally passed value of any type. | |
* @returns {value is SafeResultError} | |
* A boolean value which indicates whether the tested value is a custom `SafeResultError` type. | |
*/ | |
function isSafeResultError(value) { | |
return ( | |
Error.isError(value) && | |
getTypeSignature(value) === '[object SafeResultError]' && | |
Object.hasOwn(value, 'cause') | |
); | |
} | |
// // Extends the built-in `Error` type with another static method. | |
// Object.defineProperty(Error, 'isSafeResultError', { | |
// value: isSafeResultError, ...defaultDescriptorOptions, | |
// }); | |
/** | |
* @typedef {SafeResultError | Error} SafeResultFailType | |
*/ | |
/** | |
* @typedef {any} SafeResultPassType | |
*/ | |
/** | |
* @typedef {(value: SafeResultFailType | SafeResultPassType) => SafeResultPassType | Promise<SafeResultPassType> | void | Promise<void>} safeResultFlowCallback | |
*/ | |
/** | |
* @typedef {SafeResult<SafeResultFailType | null, SafeResultPassType | null>} SafeResultTuple | |
*/ | |
/** | |
* @typedef {SafeAsyncResult<SafeResultTuple>} SafeAsyncResultTuple | |
*/ | |
/** | |
* A `SafeResult` type represents the result of a potentially | |
* fallible operation. It stores both the potential error and | |
* the result's value in a tuple-like structure and prevents | |
* the latter's mutation. | |
* `SafeResult` furthermore does extend the native `Array`, | |
* primarily for interoperability, but does freeze the | |
* structure of each of its instances. | |
* | |
* @template E - Type of the error. | |
* @template T - Type of the result value. | |
* @extends {Array<[ E | null, T | null ]>} | |
*/ | |
class SafeResult extends Array { | |
/** | |
* Does construct a new `SafeResult` instance. | |
* | |
* @param {E | null} error | |
* The error value (if any) of the potentially failed operation. | |
* @param {T | null} value | |
* The return value (if any) of the potentially successful operation. | |
*/ | |
constructor(error, value) { | |
super(error, value); | |
// Assure an immutable `SafeResult` instance. | |
Object.freeze(this); | |
} | |
/** | |
* Ensures that array methods return plain arrays (not `SafeResult` instances). | |
* @returns {typeof Array} | |
*/ | |
static get [Symbol.species]() { | |
return Array; | |
} | |
/** | |
* The `error` property of the safe result type, exposed via a getter. | |
* @returns {E | null} | |
*/ | |
get error() { return this[0]; } | |
/** | |
* The `value` property of the safe result type, exposed via a getter. | |
* @returns {T | null} | |
*/ | |
get value() { return this[1]; } | |
/** | |
* @param {safeResultFlowCallback} callback | |
* The piped/chained function/method which is going to get handled safely. | |
* @param {any} [target] | |
* The optional `this` context of the chained and safely to be executed method. | |
* @returns {SafeResultTuple | SafeAsyncResultTuple} | |
* A `SafeResult` tuple/array either as is (as result of a non-async function call) | |
* or wrapped into a custom promise (for an async function call). | |
* The safe result's first item exclusively is either `null` or any kind of `Error` | |
* instance (including the `SafeResultError` type); its second item - the function's | |
* actually returned value - can be of any type. | |
*/ | |
pass(callback, target) { | |
const result = this; | |
return ( | |
isSafeResult(result) | |
// handle happy `then`-like `pass`-path, or keep safe-result identity for handling `fail`. | |
? ((result.error === null) && handleSafely(callback, target, result.value)) || result | |
// happy-path exception-handling. | |
: new SafeResult( | |
new TypeError( | |
'`SafeResult.prototype.pass` has to be invoked exclusively within the context of a `SafeResult` instance.' | |
), null, | |
) | |
); | |
// let result = this; | |
// | |
// if (isSafeResult(result)) { | |
// debugger; | |
// | |
// const [error, value] = result; | |
// | |
// result = (error === null) && handleSafely(callback, target, value) || result; | |
// } else { | |
// debugger; | |
// | |
// result = new SafeResult( | |
// new TypeError( | |
// '`SafeResult.prototype.pass` has to be invoked exclusively within the context of a `SafeResult` instance.' | |
// ), null, | |
// ); | |
// } | |
// return result; | |
} | |
/** | |
* @param {safeResultFlowCallback} callback | |
* The piped/chained function/method which is going to get handled safely. | |
* @param {any} [target] | |
* The optional `this` context of the chained and safely to be executed method. | |
* @returns {SafeResultTuple | SafeAsyncResultTuple} | |
* A `SafeResult` tuple/array either as is (as result of a non-async function call) | |
* or wrapped into a custom promise (for an async function call). | |
* The safe result's first item exclusively is either `null` or any kind of `Error` | |
* instance (including the `SafeResultError` type); its second item - the function's | |
* actually returned value - can be of any type. | |
*/ | |
fail(callback, target) { | |
const result = this; | |
return ( | |
isSafeResult(result) | |
// handle happy `then`-like `fail`-path, or keep safe-result identity for handling `pass`. | |
? ((result.error !== null) && handleSafely(callback, target, result.error)) || result | |
// happy-path exception-handling. | |
: new SafeResult( | |
new TypeError( | |
'`SafeResult.prototype.fail` has to be invoked exclusively within the context of a `SafeResult` instance.' | |
), null, | |
) | |
); | |
// let result = this; | |
// | |
// if (isSafeResult(result)) { | |
// debugger; | |
// | |
// const [error, value] = result; | |
// | |
// result = (error !== null) && handleSafely(callback, target, error) || result; | |
// } else { | |
// debugger; | |
// | |
// result = new SafeResult( | |
// new TypeError( | |
// '`SafeResult.prototype.fail` has to be invoked exclusively within the context of a `SafeResult` instance.' | |
// ), null, | |
// ); | |
// } | |
// return result; | |
} | |
} | |
// - Seales `SafeResult` specific constructor properties in order to achieve | |
// a stable type-identity; the base of a reliable type-detection. | |
Object.defineProperty(SafeResult.prototype, Symbol.toStringTag, { | |
get() { return 'SafeResult'; }, ...sealedDescriptorOptions, | |
}); | |
Object.defineProperty(SafeResult.prototype, 'constructor', { | |
value: SafeResult, ...sealedDescriptorOptions, writable: false, | |
}); | |
Object.defineProperty(SafeResult, 'name', { | |
value: 'SafeResult', ...sealedDescriptorOptions, writable: false, | |
}); | |
/** | |
* @param {any} [value] | |
* An optionally passed value of any type. | |
* @returns {value is SafeResult} | |
* A boolean value which indicates whether the tested value is a `SafeResult` array/tuple. | |
*/ | |
function isSafeResult(value) { | |
let proto; | |
return ( | |
Array.isArray(value) && | |
value.length === 2 && | |
Object.keys(value).length === 2 && | |
getTypeSignature(value) === '[object SafeResult]' && | |
!!(proto = Object.getPrototypeOf(value)) && | |
Object.hasOwn(proto, 'error') && | |
Object.hasOwn(proto, 'value') | |
); | |
} | |
// const promisePrototype = { | |
// then: { writable: true, enumerable: false, configurable: true, value: ƒ }, | |
// catch: { writable: true, enumerable: false, configurable: true, value: ƒ }, | |
// finally: { writable: true, enumerable: false, configurable: true, value: ƒ }, | |
// Symbol(Symbol.toStringTag): { value: 'Promise', writable: false, enumerable: false, configurable: true }, | |
// constructor: { writable: true, enumerable: false, configurable: true, value: ƒ }, | |
// }; | |
function handleSafeAsyncResultReject(/* reason */) { | |
debugger; | |
console.log('... safe-async-result :: rejectc ...', { args: arguments }); | |
throw new SafeResultError( | |
new ReferenceError( | |
'A `SafeAsyncResult` does not handle `reject`. It allways follows the happy `resolve` path.', | |
), | |
); | |
} | |
function handleSafeAsyncResultPass(callback, target, safeResult) { | |
return ( | |
isSafeAsyncResult(safeResult) | |
// unwrap. safeguard ... actively assures an always flat (hence un-nested) safe-async-result. | |
? safeResult.pass(callback, target) | |
// handle happy `then`-like `pass`-path. | |
: (safeResult.error === null) && handleSafely(callback, target, safeResult.value) | |
) || safeResult; // ... keep safe-result identity otherwise. | |
// let result; | |
// | |
// if (isSafeAsyncResult(safeResult)) { | |
// | |
// result = safeResult.pass(callback, target); | |
// | |
// } else { | |
// const [error, value] = safeResult; | |
// | |
// if (error === null) { | |
// | |
// result = handleSafely(callback, target, value); | |
// | |
// // does cause a nested safe-async-result. skip this condition. | |
// // if (isSafeResult(result)) { | |
// // result = createSafeAsyncResultFromSafeResult(result); | |
// // } | |
// } | |
// } | |
// return result || safeResult; | |
} | |
function handleSafeAsyncResultFail(callback, target, safeResult) { | |
return ( | |
isSafeAsyncResult(safeResult) | |
// unwrap. safeguard ... actively assures an always flat (hence un-nested) safe-async-result. | |
? safeResult.fail(callback, target) | |
// handle happy `then`-like `fail`-path. | |
: (safeResult.error !== null) && handleSafely(callback, target, safeResult.error) | |
) || safeResult; // ... keep safe-result identity otherwise. | |
// let result; | |
// | |
// if (isSafeAsyncResult(safeResult)) { | |
// | |
// result = safeResult.fail(callback, target); | |
// | |
// } else { | |
// const [error, value] = safeResult; | |
// | |
// if (error !== null) { | |
// | |
// result = handleSafely(callback, target, error); | |
// | |
// // does cause a nested safe-async-result. skip this condition. | |
// // if (isSafeResult(result)) { | |
// // result = createSafeAsyncResultFromSafeResult(result); | |
// // } | |
// } | |
// } | |
// return result || safeResult; | |
} | |
class SafeAsyncResult extends Promise { | |
constructor(callback) { | |
debugger;/* | |
if (isFunction(callback)) { | |
callback = (superCallback => { | |
debugger; | |
// - `reject` does not need to be handled. | |
// - `SafeAsyncResult` does always resolve. | |
return function (resolve/*, reject* /) { | |
debugger; | |
console.log('... safe-async-result :: super-callback ...'); | |
return superCallback(resolve, handleSafeAsyncResultReject); | |
}; | |
})(callback); | |
}*/ | |
// super( | |
// isFunction(callback) | |
// && (superCallback => (resolve => handleSafely(superCallback, null, resolve)))(callback) | |
// || callback | |
// ); | |
super(callback); | |
} | |
/** | |
* @param {safeResultFlowCallback} callback | |
* The piped/chained function/method which is going to get handled safely. | |
* @param {any} [target] | |
* The optional `this` context of the chained and safely to be executed method. | |
* @returns {SafeResultTuple | SafeAsyncResultTuple} | |
* A `SafeResult` tuple/array either as is (as result of a non-async function call) | |
* or wrapped into a custom promise (for an async function call). | |
* The safe result's first item exclusively is either `null` or any kind of `Error` | |
* instance (including the `SafeResultError` type); its second item - the function's | |
* actually returned value - can be of any type. | |
*/ | |
pass(callback, target) { | |
// debugger; | |
// - `SafeAsyncResult.prototype.pass` has to be implemented non-async. | |
// - Thus, one has to use its _**Thenable`**_ trait via `Promise.prototype.then`. | |
return ((isSafeAsyncResult(this) && Promise.prototype.then.call( | |
this, handleSafeAsyncResultPass.bind(null, callback, target), | |
)) || createSafeAsyncResultFromPromise( | |
Promise.resolve( | |
new SafeResult( | |
new TypeError( | |
'`SafeAsyncResult.prototype.pass` has to be invoked exclusively within the context of a `SafeAsyncResult` instance.' | |
), null, | |
), | |
), | |
)); | |
} | |
/** | |
* @param {safeResultFlowCallback} callback | |
* The piped/chained function/method which is going to get handled safely. | |
* @param {any} [target] | |
* The optional `this` context of the chained and safely to be executed method. | |
* @returns {SafeResultTuple | SafeAsyncResultTuple} | |
* A `SafeResult` tuple/array either as is (as result of a non-async function call) | |
* or wrapped into a custom promise (for an async function call). | |
* The safe result's first item exclusively is either `null` or any kind of `Error` | |
* instance (including the `SafeResultError` type); its second item - the function's | |
* actually returned value - can be of any type. | |
*/ | |
fail(callback, target) { | |
// debugger; | |
// - `SafeAsyncResult.prototype.fail` has to be implemented non-async. | |
// - Thus, one has to use its _**`Thenable`**_ trait via `Promise.prototype.then`. | |
return ((isSafeAsyncResult(this) && Promise.prototype.then.call( | |
this, handleSafeAsyncResultFail.bind(null, callback, target), | |
)) || createSafeAsyncResultFromPromise( | |
Promise.resolve( | |
new SafeResult( | |
new TypeError( | |
'`SafeAsyncResult.prototype.fail` has to be invoked exclusively within the context of a `SafeAsyncResult` instance.' | |
), null, | |
), | |
), | |
)); | |
} | |
get then() { return void 0; } | |
get catch() { return void 0; } | |
get finally() { return void 0; } | |
} | |
// - Seales `SafeAsyncResult` specific constructor properties in order to achieve | |
// a stable type-identity; the base of a reliable type-detection. | |
Object.defineProperty(SafeAsyncResult.prototype, Symbol.toStringTag, { | |
get() { return 'SafeAsyncResult'; }, ...sealedDescriptorOptions, | |
}); | |
Object.defineProperty(SafeAsyncResult.prototype, 'constructor', { | |
value: SafeAsyncResult, ...sealedDescriptorOptions, writable: false, | |
}); | |
Object.defineProperty(SafeAsyncResult, 'name', { | |
value: 'SafeAsyncResult', ...sealedDescriptorOptions, writable: false, | |
}); | |
/** | |
* @param {any} [value] | |
* An optionally passed value of any type. | |
* @returns {value is SafeAsyncResult} | |
* A boolean value which indicates whether the tested value is a `SafeAsyncResult` array/tuple. | |
*/ | |
function isSafeAsyncResult(value) { | |
let proto; | |
return ( | |
getTypeSignature(value) === '[object SafeAsyncResult]' && | |
!!(proto = Object.getPrototypeOf(value)) && | |
isPromise(Object.getPrototypeOf(proto)) && | |
Object.hasOwn(proto, 'pass') && | |
Object.hasOwn(proto, 'fail') && | |
isFunction(value.pass) && | |
isFunction(value.fail) | |
); | |
} | |
// on ... "handle safely" versus "process safely" | |
// | |
// - With a utility-function at framework or runtime-level, one might want to avoid reusing | |
// the ubiquitous "handle". Then `processSafely` might be preferred over `handleSafely`. | |
// The former in addition comes with a slightly more data/flow-processing vibe. | |
// | |
// - In case one exclusively does focus on a name that cleanly fits both promises and functions | |
// and is idiomatic to JavaScript/TypeScript, one should just go with `handleSafely`. | |
// | |
function createSafeAsyncResultFromSafeResult(result) { | |
// debugger; | |
return new SafeAsyncResult(resolve => resolve(result)); | |
} | |
function createSafeAsyncResultFromPromise(proceed) { | |
// debugger; | |
return new SafeAsyncResult(async (resolve) => { | |
let value = null; | |
let error = null; | |
// debugger; | |
try { | |
value = await proceed; | |
} catch (cause) { | |
error = (Error.isError(cause) && cause) || new SafeResultError(cause); | |
} | |
resolve(new SafeResult(error, value)); | |
}); | |
} | |
function createSafeAsyncResultFromAsyncFunction(proceed, target, ...args) { | |
// debugger; | |
return new SafeAsyncResult(async (resolve) => { | |
let value = null; | |
let error = null; | |
// debugger; | |
try { | |
value = await proceed.apply(target ?? null, args); | |
} catch (cause) { | |
error = (Error.isError(cause) && cause) || new SafeResultError(cause); | |
} | |
resolve(new SafeResult(error, value)); | |
}); | |
} | |
/** | |
* Handles/processes any function/method (async or not) and any promise safely | |
* whilst preserving a method's `this` context. | |
* The possible throwing of a processed function/promise gets handled silently | |
* and is reflected by the always returned structured `SafeResult` array/tuple. | |
* The latter gets returned either as is (as result of a non-async function call), | |
* or it gets wrapped into a custom promise (reflecting the processing of either an async | |
* function or a promise). | |
* | |
* @proceed {SafeResult | SafeAsyncResult | Promise | AsyncFunction | Function} | |
* The function/method or (custom) promise which is going to get handled safely. | |
* @param {any} target | |
* The `this` context of the safely to be executed/handled method. | |
* @param {...any} args | |
* The arguments of the safely to be executed/handled function/method. | |
* @returns {SafeResultTuple | SafeAsyncResultTuple} | |
* A `SafeResult` tuple/array either as is (as result of a non-async function call) | |
* or wrapped into a custom promise (having processed either an async function or a promise). | |
* The safe result's first item exclusively is either `null` or any kind of `Error` | |
* instance (including the `SafeResultError` type); its second item - the function's | |
* or the promise's actually returned respectively resolved value - can be of any type. | |
*/ | |
function handleSafely(proceed, target, ...args) { | |
let result = null; | |
if (isFunction(proceed)) { | |
if (!isAsyncFunction(proceed)) { | |
// debugger; | |
let value = null; | |
let error = null; | |
try { | |
value = proceed.apply(target ?? null, args); | |
} catch (cause) { | |
error = (Error.isError(cause) && cause) || new SafeResultError(cause); | |
} | |
if (isPromise(value)) { | |
// debugger; | |
result = createSafeAsyncResultFromPromise(value); | |
} else { | |
// debugger; | |
result = new SafeResult(error, value); | |
} | |
} else { | |
// debugger; | |
result = createSafeAsyncResultFromAsyncFunction(proceed, target, ...args); | |
} | |
} else if (isPromise(proceed)) { | |
// debugger; | |
result = createSafeAsyncResultFromPromise(proceed); | |
} else if ( | |
isSafeResult(proceed) || | |
isSafeAsyncResult(proceed) | |
) { | |
// debugger; | |
// -- IDENTITY -- | |
result = proceed; | |
} else { | |
// debugger; | |
result = new SafeResult( | |
new TypeError( | |
'`handleSafely` does process exclusively promises and callable function types, async or non-async alike, as well as safe-result instances, either async or not.' | |
), null, | |
); | |
} | |
debugger; | |
return result; | |
} | |
/** | |
* Executes any non-async function/method safely whilst preserving its `this` context. | |
* The possible throwing of an executed function gets handled silently and reflected | |
* by the always returned structured `SafeResult` array/tuple. | |
* | |
* @this {Function} | |
* The function/method which gets operated/executed by `safe`. | |
* @param {any} target | |
* An additionally to be passed target, the `this` context of | |
* the safely to be executed method. | |
* @param {...any} args | |
* The arguments of the safely to be executed function/method. | |
* @returns {SafeResultTuple} | |
* Always a `SafeResult` tuple/array where its first item exclusively is either `null` | |
* or any kind of `Error` instance (including the `SafeResultError` type), and where | |
* its second item - the function's actually returned result - can be of any type. | |
*/ | |
function safe(target, ...args) { | |
/** @type {Function} */ | |
const proceed = this; | |
let result = null; | |
let error = null; | |
if (isFunction(proceed) && !isAsyncFunction(proceed)) { | |
try { | |
result = proceed.apply(target ?? null, args); | |
} catch (cause) { | |
error = (Error.isError(cause) && cause) || new SafeResultError(cause); | |
} | |
} else { | |
// improper usage, like an explicit delegation to a wrong type, gets thrown immediately. | |
throw new TypeError( | |
'`Function.prototype.safe` has to be exclusively invoked at a non-async function type.' | |
); | |
} | |
return new SafeResult(error, result); | |
} | |
/** | |
* Executes any callable function/method asynchronously and safely whilst preserving | |
* its `this` context. | |
* The possible throwing of an executed function gets handled silently and reflected | |
* by the always returned structured `SafeResult` array/tuple. | |
* | |
* @this {AsyncFunction | Function} | |
* The function/method which gets asynchronously operated/executed by `safeAsync`. | |
* @param {any} target | |
* An additionally to be passed target, the `this` context of the asynchronously | |
* and safely to be executed method. | |
* @param {...any} args | |
* The arguments of the asynchronously and safely to be executed function/method. | |
* @returns {SafeAsyncResultTuple} | |
* Always a resolved promise which wraps a `SafeResult` tuple/array where its first | |
* item exclusively is either `null` or any kind of `Error` instance (including the | |
* `SafeResultError` type), and where its second item - the function's actually | |
* returned result - can be of any type. | |
*/ | |
async function safeAsync(target, ...args) { | |
/** @type {AsyncFunction | Function} */ | |
const proceed = this; | |
let result = null; | |
let error = null; | |
if (isFunction(proceed)) { | |
try { | |
result = await proceed.apply(target ?? null, args); | |
} catch (cause) { | |
error = (Error.isError(cause) && cause) || new SafeResultError(cause); | |
} | |
} else { | |
// improper usage, like an explicit delegation to a wrong type, gets thrown immediately. | |
throw new TypeError( | |
'`AsyncFunction.prototype.safe` has to be exclusively invoked at a function type.' | |
); | |
} | |
// The return value automatically gets wrapped into a resolved promise. | |
// It's the implicit form of the very explicit ... | |
// `return Promise.resolve(new SafeResult(error, result));` | |
return new SafeResult(error, result); | |
} | |
/** | |
* Awaits/handles any promise safely. The possible throwing of an awaited promise gets | |
* handled silently and reflected by the always returned structured `SafeResult` array. | |
* | |
* @returns {SafeAsyncResultTuple} | |
* Always a resolved promise which wraps a `SafeResult` tuple/array where its first | |
* item exclusively is either `null` or any kind of `Error` instance (including the | |
* `SafeResultError` type), and where its second item - the promise's actually | |
* resolved value - can be of any type. | |
*/ | |
async function safeAwait() { | |
/** @type {Promise} */ | |
const promise = this; | |
let resolved = null; | |
let error = null; | |
if (isPromise(promise)) { | |
try { | |
resolved = await promise; | |
} catch (cause) { | |
error = (Error.isError(cause) && cause) || new SafeResultError(cause); | |
} | |
} else { | |
// improper usage, like an explicit delegation to a wrong type, gets thrown immediately. | |
throw new TypeError( | |
'`Promise.prototype.safe` has to be exclusively invoked at a `Promise` instance.' | |
); | |
} | |
// The return value automatically gets wrapped into a resolved promise. | |
// It's the implicit form of the very explicit ... | |
// `return Promise.resolve(new SafeResult(error, resolved));` | |
return new SafeResult(error, resolved); | |
} | |
// Rewrites and seales the `safe` function's name property in order to support a stable type-detection. | |
Object.defineProperty(safe, 'name', { | |
value: 'safe', ...sealedDescriptorOptions, writable: false, | |
}); | |
// Extends `Function.prototype` with a `safe` method that returns a `SafeResult`. | |
Object.defineProperty(Function.prototype, 'safe', { | |
value: safe, ...defaultDescriptorOptions, | |
}); | |
// Rewrites and seales the `safeAsync` function's name property in order to support a stable type-detection. | |
Object.defineProperty(safeAsync, 'name', { | |
value: 'safe', ...sealedDescriptorOptions, writable: false, | |
}); | |
// Extends `AsyncFunction.prototype` with a `safe` method that returns a `SafeResult`. | |
Object.defineProperty((async function () {}).constructor.prototype, 'safe', { | |
value: safeAsync, ...defaultDescriptorOptions, | |
}); | |
// Rewrites and seales the `safeAwait` function's name property in order to support a stable type-detection. | |
Object.defineProperty(safeAwait, 'name', { | |
value: 'safe', ...sealedDescriptorOptions, writable: false, | |
}); | |
// Extends `Promise.prototype` with a `safe` method that returns a `SafeResult`. | |
Object.defineProperty(Promise.prototype, 'safe', { | |
value: safeAwait, ...defaultDescriptorOptions, | |
}); | |
// ----- ----- ----- ----- ----- | |
const [fetchException, fetchResponse] = | |
await handleSafely(fetch, null, 'https://jsonplaceholder.typicode.com/posts'); | |
const [error, data] = (fetchException === null) | |
&& ( | |
fetchResponse.ok | |
&& await handleSafely(fetchResponse.json()) | |
|| ( | |
fetchResponse.status === 404 | |
&& new SafeResult(new NotFoundError(fetchResponse), null) | |
|| new SafeResult(new HttpError(fetchResponse), null) | |
) | |
) || new SafeResult(fetchException, fetchResponse); | |
console.log({ error, data }); | |
const [cause, value] = await handleSafely( | |
fetch, null, 'https://jsonplaceholder.typicode.com/posts' | |
).pipe(async (response) => { | |
if (response.ok) { | |
return await response.json(); | |
} else if (response.status === 404) { | |
throw new NotFoundError(response); | |
} else { | |
throw new HttpError(response); | |
} | |
}) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// safe-result.d.ts | |
/** | |
* Returns the internal [[Class]] tag of a value, like `[object String]`. | |
* Returns `undefined` if no argument is passed. | |
* | |
* @param args - A variadic argument list. | |
*/ | |
export function getTypeSignature(...args: any[]): string | undefined; | |
/** | |
* Determines if a value is a function with standard call/apply/bind methods. | |
*/ | |
export function isFunction(value?: any): value is Function; | |
/** | |
* Determines if a value is a native async function. | |
*/ | |
export function isAsyncFunction(value?: any): value is (...args: any[]) => Promise<any>; | |
/** | |
* Determines if a value is a Promise instance. | |
*/ | |
export function isPromise(value?: any): value is Promise<any>; | |
/** | |
* Determines if a value is a string (primitive or object-wrapped). | |
*/ | |
export function isString(value?: any): value is string; | |
/** | |
* Determines if a value is a custom `SafeResultError` instance. | |
*/ | |
export function isSafeResultError(value?: any): value is SafeResultError; | |
/** | |
* Determines if a value is a `SafeResult` instance. | |
*/ | |
export function isSafeResult(value?: any): value is SafeResult<Error | SafeResultError | null, any>; | |
/** | |
* Custom error class that wraps non-error exceptions with a cause. | |
*/ | |
export class SafeResultError extends Error { | |
constructor(cause: any); | |
get name(): 'SafeResultError'; | |
} | |
/** | |
* Represents the result of a potentially fallible operation. | |
* It stores both an error and a value, and is immutable. | |
* | |
* @template E - Type of the error. | |
* @template T - Type of the result value. | |
*/ | |
export class SafeResult<E = Error | SafeResultError | null, T = any> extends Array<[E | null, T | null]> { | |
constructor(error: E | null, value: T | null); | |
get error(): E | null; | |
get value(): T | null; | |
static readonly [Symbol.species]: typeof Array; | |
} | |
/** | |
* Shorthand alias for the common SafeResult tuple structure. | |
*/ | |
export type SafeResultTuple = SafeResult<SafeResultError | Error | null, any>; | |
/** | |
* Safely executes any non-async function with a preserved `this` context. | |
* | |
* This method is intended to be used via `Function.prototype.safe`. | |
* | |
* @param target - The value to use as `this`. | |
* @param args - Arguments passed to the function. | |
* @returns A `SafeResultTuple` containing error and result. | |
*/ | |
export function safe(this: Function, target?: any, ...args: any[]): SafeResultTuple; | |
/** | |
* Safely executes an async or sync function with a preserved `this` context. | |
* | |
* This method is intended to be used via `AsyncFunction.prototype.safe`. | |
* | |
* @param target - The value to use as `this`. | |
* @param args - Arguments passed to the function. | |
* @returns A Promise resolving to a `SafeResultTuple`. | |
*/ | |
export function safeAsync(this: Function, target?: any, ...args: any[]): Promise<SafeResultTuple>; | |
/** | |
* Safely awaits any Promise. | |
* | |
* This method is intended to be used via `Promise.prototype.safe`. | |
* | |
* @returns A Promise resolving to a `SafeResultTuple`. | |
*/ | |
export function safeAwait(this: Promise<any>): Promise<SafeResultTuple>; | |
// Prototype extensions for IDE support | |
declare global { | |
interface Function { | |
safe: typeof safe; | |
} | |
interface Promise<T> { | |
safe: typeof safeAwait; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment