Skip to content

Instantly share code, notes, and snippets.

@theScottyJam
Last active October 13, 2021 23:08
Show Gist options
  • Save theScottyJam/fee1a93db153d87fbbf3d45fc0241c49 to your computer and use it in GitHub Desktop.
Save theScottyJam/fee1a93db153d87fbbf3d45fc0241c49 to your computer and use it in GitHub Desktop.
Light version of the explicit-exceptions API. Designed for those would rather maintain a simplified version of the library than add it as a dependency. This gist is licensed under the MIT license.
'use strict'
// This module provides a light version of the explicit-exceptions API.
// You can find the original package this module was modeled after
// at this URL: https://www.npmjs.com/package/explicit-exceptions
/**
* An exception that can be thrown (or rethrown) within a function decorated with wrap().
*/
class Exception extends Error {
/**
* @param {string} code - An exception code to distinguish it from other exceptions. (e.g. NotFound)
* @param {string} [message] - Optional exception reason, to help with debugging. It's especially useful for when the exception is escalated into a fatal error.
* @param {any} data - arbitrary data that you wish to attach to the exception object.
*/
constructor(code, message = null, data = null) {
super(message ? `${code}: ${message}` : code)
this.name = 'Exception'
this.code = code
this.data = data
Object.freeze(this)
}
}
class Ok {
constructor(data) {
this.data = data
}
}
/**
* Unwraps an exception or ok type. If the value is an exception, it will be rethrown, otherwise, it will be returned.
* @param {(Exception|Ok)}
* @param {string[]} [allowedExceptionTypes=[]] An exception will only be rethrown if it's exception-code is found in this list. Otherwise, it'll be escalated to a fatal error.
*/
function unwrap(errOrOk, allowedExceptionTypes = []) {
if (errOrOk instanceof Ok) {
return errOrOk.data
} else if (errOrOk instanceof Exception) {
if (exceptionInTypeList(errOrOk, allowedExceptionTypes)) throw errOrOk
throw escalatedException(errOrOk)
} else {
throw new Error('Invalid Parameter')
}
}
/**
* Decorates the provided wrappedFn, such that all returned values will be wrapped in an OK instance, and all thrown exceptions will be caught and returned.
* unwrap() can be used to retrieve the original returned value, while handling any exceptions that may have occured.
* @param {function} wrappedFn
* @param {string[]} [allowedExceptionTypes] Optional list of exceptions this function may propagate. Defaults to allowing all.
* @returns {(Exception|Ok)}
*/
function wrap(wrappedFn, allowedExceptionTypes = null) {
if (isAsyncFn(wrappedFn)) {
throw new Error('Attempted to call wrap() on an async function. Use wrapAsync() instead.')
}
return function wrapped(...args) {
try {
return new Ok(wrappedFn(...args))
} catch (ex) {
if (!(ex instanceof Exception)) throw ex
if (allowedExceptionTypes == null || exceptionInTypeList(ex, allowedExceptionTypes)) {
return ex
}
throw escalatedException(ex)
}
}
}
/**
* Same as wrap(), but will correctly wrap an async function.
* @returns {Promise} A promise resolving in an Exception or Ok type
*/
function wrapAsync(wrappedFn, allowedExceptionTypes = null) {
return async function wrapped(...args) {
try {
return new Ok(await wrappedFn(...args))
} catch (ex) {
if (!(ex instanceof Exception)) throw ex
if (allowedExceptionTypes == null || exceptionInTypeList(ex, allowedExceptionTypes)) {
return ex
}
throw escalatedException(ex)
}
}
}
// This function does a best effort attempt at determining if a function is async.
// It will not return true for transpiles async functions or functions that just return promises
const isAsyncFn = fn => fn instanceof (async () => {}).constructor
const exceptionInTypeList = (ex, types) => types.find(type => ex.code === type)
const escalatedException = ex => new Error(ex.message)
module.exports = { Exception, unwrap, wrap, wrapAsync }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment