catch Error

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:

try {
  foo();
} catch Error (e) {
  console.log(e.stack);
}

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.

1 Like

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
2 Likes

Be aware that instanceOf checks do not work across Realms, e.g an error that comes from a different iFrame.

1 Like

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.

1 Like

@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}`)
}
1 Like

In node, this can also happen from using the vm module.

2 Likes

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).

2 Likes

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.

That's honestly how I would have interpreted the initial idea for this proposal, if I saw that syntax without context.

Catch guards are a useful proposal, and that’s what this looks like. Coercing to an error doesn’t seem valuable to me.

1 Like

If we change it to @theScottyJam's idea, maybe we'd also get catch guards almost for free:

const rethrowIfNot = type => err => {
  if (err instanceof type) {
    return err
  }
  throw err
}

try {
  foo();
} catch (err from rethrowIfNot(TypeError)) {
  console.log('type error!')
}

Although it wouldn't allow for multiple catches.

1 Like

I created a new topic; I officially disown this one: Parameter coercion