The author fails to acknowledge that Go’s approach to error handling, which forms a core feature of the language, likely inspired the article’s subject/idea. Furthermore, there is no mention that the article’s topic might have been influenced or borrowed from an early proposal for a safe assignment operator (?=
), which has not yet been officially adopted.
Additionally, the solution proposed by the author requires two separate functions: safeAssign
and safeAssignAsync
. This approach is less than ideal as it does not provide a unified solution capable of handling both synchronous and asynchronous functions seamlessly.
For those who do not wish to wait for the potential introduction of the ?=
operator—which might never come—a better alternative would be to implement a prototype-based method. This method could be similar to existing ones like Function.prototype.call
and Function.prototype.apply
.
It could be named, for instance, execute
, and implemented as Function.prototype.execute
for synchronous functions and AsyncFunction.prototype.execute
for asynchronous ones.
A ready-to-use implementation might look as follows:
function exposeTypeSignature(value) {
return Object.prototype.toString.call(value);
}
function isAsyncFuntionType(value) {
return exposeTypeSignature(value) === '[object AsyncFunction]';
}
function isFunction(value) {
return (
typeof value === 'function' &&
typeof value.call === 'function' &&
typeof value.apply === 'function'
);
}
function execute/*Safely*/(target, ...args) {
const proceed = this;
let result = null;
let error = null;
if (isFunction(proceed)) {
if (isAsyncFuntionType(proceed)) {
error = new TypeError(
'The non-async `execute` exclusively can be invoked at non-async callable types.'
);
} else {
try {
result = proceed.apply(target ?? null, args);
} catch (/** @type {Error} */exception) {
error = exception;
}
}
} else {
error = new TypeError(
'`execute` exclusively can be invoked at a callable type.'
);
}
return [/** @type {null|Error} */error, result];
}
async function executeAsync/*Safely*/(target, ...args) {
const proceed = this;
let result = null;
let reason = null;
let error = null;
if (isFunction(proceed)) {
try {
result = await proceed.apply(target ?? null, args);
} catch (/** @type {string|Error} */exception) {
reason = exception;
}
} else {
error = new TypeError(
'`execute` exclusively can be invoked at a callable type.'
);
}
return [/** @type {null|string|Error} */reason ?? error, result];
}
Reflect.defineProperty(Function.prototype, 'execute', {
value: execute,
writable: true,
configurable: true,
});
Reflect.defineProperty((async function () {}).constructor.prototype, 'execute', {
value: executeAsync,
writable: true,
configurable: true,
});
And the minimum test case for this solution would be as simple and convenient as:
function proveSuccess(value) {
return value;
}
function proveFailure() {
throw new Error('failure');
}
async function proveSuccessAsync(value) {
return new Promise(resolve => setTimeout(resolve, 2000, value));
}
async function proveFailureAsync() {
return new Promise((_, reject) => setTimeout(reject, 2000, 'rejected'));
}
let reason, error, result;
([error, result] = proveSuccess.execute(null, 'successfully executed'));
console.log({ error, result });
([error, result] = proveFailure.execute());
console.log({ error, result });
([error, result] = await proveSuccessAsync.execute(null, 'async success'));
console.log({ error, result });
([reason, result] = await proveFailureAsync.execute());
console.log({ reason, result });