Skip to content

Instantly share code, notes, and snippets.

@devinrhode2
Last active December 30, 2022 20:23
Show Gist options
  • Save devinrhode2/25425538daf02d74f9889648ac8f579e to your computer and use it in GitHub Desktop.
Save devinrhode2/25425538daf02d74f9889648ac8f579e to your computer and use it in GitHub Desktop.
safely call function, and return `returnValue | Error` https://www.swyx.io/errors-not-exceptions
/**
* Calls `callback`, catches any errors and returns `returnValue | Error`
*
* TODO: create npm package
*
* See: https://dev.to/devinrhode2/comment/1fcga
*/
export const safeCall = <
TCallback extends (
...args: unknown[]
) => unknown,
>(
callback: TCallback,
): ReturnType<TCallback> | Error => {
let error = undefined as unknown // it can actually stay undefined, which means no error was thrown
let returnValue = undefined as
| ReturnType<TCallback>
| undefined
let didThrow = false
try {
returnValue =
callback() as ReturnType<TCallback>
} catch (e) {
didThrow = true
error = e
}
if (didThrow) {
if (!(error instanceof Error)) {
throw new Error(
[
`Caught 💩 which is not an instanceof Error: ${error}`,
'You should never throw, but if you have no choice, this is how you do it: throw new Error("Whoops");',
'This gives you accurate stack traces which point back to the original source of the exception.',
].join('\n'),
)
}
return error
}
return returnValue
}
type MaybeNewNodes = [TreeNode[], undefined] | [undefined, Error];
/**
* TS didn't narrow types inside of mutateNodes when directly embedding the validations above
* Therefore, pulled it into a different function, to avoid sprinkling assertions below.
*/
const getNewNodesInner = (
newValues: NewNodeFromServer,
primaryNodes: TreeNode[],
): MaybeNewNodes => {
const primaryNodesCopy = [...primaryNodes];
const { id } = newValues;
if (id === LoadMoreTreeNodeId) {
return [
undefined,
new Error(
`Server appears to have sent \`"id": "${LoadMoreTreeNodeId}"\`, this should never happen`,
),
];
}
let isTreeUpdated = false;
/**
* Find `id` and mutate `tree.children .members + .view`
*/
function mutateNodes(tree: TreeNode): boolean {
if (tree.type === LoadMoreTreeNodeType) {
// LoadMoreTreeNode's have no children to further traverse into.
return false;
}
// Note: id will never be `${LoadMoreTreeNodeId}`
if (tree.id && id.includes(tree.id)) {
// One example:
// id:
// /account/someco-internal/children
// tree.id:
// /account/someco-internal
// legacy tree patching code: https://github.com/SomeCo/tesseract/commit/3734c554b4b2c0655625dec020bfce88c5a1799b
if (tree.id === id) {
reportError(
new Error(
[
`The "${tree.id}" account appears to be trying to update children`,
`with the exact same id. This is not expected.`,
].join(""),
),
);
// keep isTreeUpdated as false
// Stop searching.
return true;
}
if (JSON.stringify(tree.children) !== JSON.stringify(newValues)) {
// eslint-disable-next-line no-param-reassign
tree.children = newValues;
isTreeUpdated = true;
}
return true;
}
// Keep trying to traverse tree:
// NOTE: may not need optional chaining here, we should remove it once we have e2e type safety.
return tree.children?.members?.some((child) => {
return mutateNodes(child);
});
}
const isIdFound = primaryNodesCopy.some((node) => {
return mutateNodes(node);
});
if (isIdFound) {
if (isTreeUpdated) {
return [primaryNodesCopy, undefined];
}
// Skipped update, nodes have not changed
// TODO: change to:
// return { warning: 'update_skipped' }
return [undefined, undefined];
}
return [
undefined,
new Error(
[
// prettier-preserve-newline
"Could not find `id` in tree:",
JSON.stringify(newValues, null, 2),
].join("\n"),
),
];
};
@devinrhode2
Copy link
Author

A more reasonable return type could be:

type r = [ReturnType<TCallback>, undefined] | [undefined, Error]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment