Catching errors without taking unsafe shortcuts is pretty awkward. Since anything can be thrown, we have to write type checks before dealing with Error instances. This is usually not well tested code since it's unusual for non-Errors to be thrown. Typescript users are extra aware of this problem, especially when using lint rules that prevent implicit/explicit any. What if we could do:
Where Error could be any identifier. The above would be syntactic sugar for
try {
foo();
} catch (_e) {
let e = Error[Symbol.errorFromUnknown](_e);
console.log(e.stack);
}
And [Symbol.errorFromUnknown] would be a new static property on Error looking something like:
val => val instanceof Error ? val : new Error(`Non-error object: ${val}`)
Users would be free to define their own classes with similar properties that could wrap Errors in bespoke ways:
class MyCompanyError extends Error {
constructor(message, cause) {
super(message)
this.cause = cause
}
static [Symbol.errorFromUnknown](val) {
if (val instanceof MyCompanyError) {
return val
}
if (val instanceof Error) {
return new MyCompanyError('foo', val)
}
return new MyCompanyError ('bar')
}
}
In typescript, the type could be inferred from the return type of of the [Symbol.errorFromUnknown] function and no more awkward annotations or lint suppressions would be needed.
Interesting idea. This sounds like an issue that could potentially be solved in a more general purpose way. For example, I know it's common for a function that expects a string to take whatever parameter it recieves and coerce it into a string, as a first step, like this:
function concat(x_, y_) {
const x = String(x_)
const y = String(y_)
return x + y
}
What if, at any location where you're binding a value to a variable, you're allowed to pass the incomming value through a normalization function first, through, say, a "from" keyword (I don't like this "from" word - feel free to bikeshed it). That would make the above example equivalent to this:
function concat(x from String, y from String) {
return x + y
}
It would also allow you to automatically coerce an unknown value to an error.
try {
foo();
} catch (e from Error) {
console.log(e.stack);
}
// ... is the same as ...
try {
foo();
} catch (e_) {
let e = Error(e_)
console.log(e.stack);
}
Some more usage examples :
const normalizedDegrees = deg => deg % 360
function toRadians(deg from normalizedDegrees) {
// ...
}
const positiveNumberAssertion = value => {
if (value <= 0) throw new Error('Whoops!')
return value
}
function doOperation(x from positiveNumberAssertion) {
// ...
}
const {
index from i => i + 1,
...otherParams
} = data
Be aware that instanceOf checks do not work across Realms, e.g an error that comes from a different iFrame.
@aclaymore interesting, I'm mostly writing server-side js so haven't come across this. Although the actual implementation of the Error-coercer is just a strawman. It could check that the shape is error-like instead of using instanceof.
@theScottyJam I love that idea. That could effectively give opt-in type checking to plain javascript. I feel like you could go really, really far with it. If it were implemented, eventually type annotations might not be needed in application code by using a runtime type checking library:
import { z } from 'zod'
const User = z.object({
name: z.string(),
email: z.string().email(),
})
const greetUser = (user from User.parse) => {
console.log(`Hello ${user.name}, your email is ${user.email}`)
}
Got it. I prefer the broader parameter coercion idea anyway, so I think I'll edit the original post. That way the specific error coercion function is removed from scope. The error cause proposal could be a good way of making a wrapper that coerces to an Error safely. The original error from another realm could become the cause. But again, with @theScottyJam 's idea this proposal wouldn't really need to worry about that (it could just motivate an "official" Error coercer function in a separate mini-proposal).
Is there a risk that someone could interpret this as something that only catches Error? Similar to how catch works in languages like c++ and Java when the type is on the other side of the open bracket.