Dealing with a value that could be either a exception or Js.Exn?

Express5 exception middleware conveniently automatically catches all exceptions and passes them to an error handler in the first argument

binding example:

  Express5.useError(express, (err, _req, res, _next) => {
    // would like to do something smart with err here so
    // I can do more than return {"error": true}
    Express5.json(res, {"error": true})
  })

I’m not sure how to handle this. Is there a specific type for err that works with both rescript exceptions and javascript exceptions?

Can I switch on it? Can I maybe rethrow err from within a switch statement like this?

  Express5.useError(express, (err, _req, res, _next) => {
    try {
      raise(err)
    } catch {
    | Not_found => ...
    | Invalid_argument(_) => ...
    | Js.Exn.Error(obj) => ...
    }
  })

There is an exception switch statement variant. Not 100% sure if it applies here or not

switch await authenticate() {
  | _ => Js.log("ok")
  | exception (e) => /**...*/
}

You can see it used here in the second example

1 Like

I’m aware, but the problem is I’m not catching an exception, its being stuck into a variable and passed to a callback. I dont know how to type err so it can be used in a catch/switch statement.

…Ok I made a little progress, I can give it the type exn in the binding. exn is some mystery type I got from hovering over it, that seems to play nice with the switch statement

type err = exn
@send external useError: (t, (err, req, res, unit) => unit) => unit = "use"

But now the problem is

  Express5.useError(express, (err, _req, res, _next) => {
    switch err {
    | exception Not_found => Console.log("not found")
    | exception Invalid_argument(x) => Console.log2("invalid argument", x)
    | exception x => Console.log2("exn catch all", x)
    | Js.Exn.Error(obj) => Console.log2("js object", obj)
    | _ => Console.log2("exn mystery meat", err)
    }
    Express5.json(res, {"error": true})
  })

only matches the catch all, whether I raise an rescript exception or a js exception. The js output looks dubious too, but I wont paste that here.

…made some more progress, by raising the error again

  Express5.useError(express, (err, _req, res, _next) => {
    try {
      raise(err)
    } catch {
    | Not_found => Console.log("not found")
    | Invalid_argument(x) => Console.log2("invalid argument", x)
    | Js.Exn.Error(obj) => Console.log2("js object", obj)
    | _ => Console.log2("exn mystery meat", err)
    }
    Express5.json(res, {"error": true})
  })

  express
}

Now, assuming the above is not a coding horror, I need to be able to extract something meaningful from the catch all clause since I’m sure I didn’t handle all possible exceptions thrown (except for the Js ones)

What is the shape of the error when you log the catch all?

Its either one of these guys, when its thrown from something like Option.getExn (which I like using because bailing out when something is wrong is so much easier when dealing with back end)

{
  RE_EXN_ID: 'Not_found',
  Error: Error
      at Module.getExn (file:///Users/kevin/Dev/wheatt/node_modules/@rescript/core/src/Core__Option.bs.mjs:25:16)...
}

Or its just a js Error object with the usual methods like .name() and .message() when its thrown from some JS library.

Here’s what I did for anybody interested, in the bindings there’s a type exn which seems to work with both rescript variant type errors and JS exception type errors. exn doesn’t seem to have any documentation, it just appears in tooltips.

Express5.res

type err = exn
@send external useError: (t, (err, req, res, unit) => unit) => unit = "use"

In the error handler below the hack was to rethrow the error and do the usual try/catch switch. Its interesting that the JS exceptions are more useful than the rescript error variants because they hold more information.

let handleError = (express: t) => {
  express->useError((err, _req, res, _next) => {
    let uuid = randomUUID()
    try {
      raise(err)
    } catch {
    | Js.Exn.Error(obj) => Console.log4(uuid, Exn.name(obj), Exn.message(obj), Exn.stack(obj))
    | _ => Console.log3(uuid, "rescript exn mystery meat", err)
    }
    res->json({"error-logged": uuid})
  })
}

its hooked up like this (put the error handler after the routes, its the express.js rules)

Server.res

express->Routes.initialize
express->Express5.handleError

Express like web frameworks were not built with rescript in mind so error handling at a distance is a little awkward.

Somebody please write an ergonomic rescript only backend web framework please.

can you show me the log without raising it please?

Not quite sure what you are asking, but I think you want a console.log of the error passed to the express middleware error handler without modification or reraising

so if this is the middleware error handler, pure console.log

@send external useError: (t, (err, req, res, unit) => unit) => unit = "use"
let handleError = (express: t) => {
  express->useError((err, _req, res, _next) => {
    Console.log(err)
  })
}

If I raise from my version Option.getExn that throws JS Exceptions instead

external dictToJSON: Js.Dict.t<string> => JSON.t = "%identity"
let getKeyJsExn = (d: Dict.t<string>, key: string): string => {
  switch Dict.get(d, key) {
  | Some(value) => value
  | None =>
    let d = dictToJSON(d)
    Js.Exn.raiseError(`Validation error, ${key} is None, ${JSON.stringify(d)}`)
  }
}

I get this

[backend:dev] Error: Validation error, password is None, {"email":"me@example.com","not_password":"asdf"}
[backend:dev]     at Module.raiseError (file:///Users/kevin/Dev/demo/node_modules/rescript/lib/es6/js_exn.js:5:9)
[backend:dev]     at Module.getKeyJsExn (file:///Users/kevin/Dev/demo/backend/src/interop/Express5.bs.mjs:30:19)
[backend:dev]     at auth (file:///Users/kevin/Dev/demo/backend/src/routes/PublicRoutes.bs.mjs:20:27)
[backend:dev]     at Layer.handleRequest (/Users/kevin/Dev/demo/node_modules/router/lib/layer.js:101:15)
[backend:dev]     at next (/Users/kevin/Dev/demo/node_modules/router/lib/route.js:142:13)
[backend:dev]     at Route.dispatch (/Users/kevin/Dev/demo/node_modules/router/lib/route.js:107:3)
[backend:dev]     at handle (/Users/kevin/Dev/demo/node_modules/router/index.js:421:11)
[backend:dev]     at Layer.handleRequest (/Users/kevin/Dev/demo/node_modules/router/lib/layer.js:101:15)
[backend:dev]     at /Users/kevin/Dev/demo/node_modules/router/index.js:288:22
[backend:dev]     at processParams (/Users/kevin/Dev/demo/node_modules/router/index.js:568:12)

If I raise from this

let password = Dict.get(fields, "password")->Option.getExn

I get this

[backend:dev] {
[backend:dev]   RE_EXN_ID: 'Not_found',
[backend:dev]   Error: Error
[backend:dev]       at Module.getExn (file:///Users/kevin/Dev/wheatt/node_modules/@rescript/core/src/Core__Option.bs.mjs:25:16)
[backend:dev]       at auth (file:///Users/kevin/Dev/wheatt/backend/src/routes/PublicRoutes.bs.mjs:21:31)
[backend:dev]       at Layer.handleRequest (/Users/kevin/Dev/wheatt/node_modules/router/lib/layer.js:101:15)
[backend:dev]       at next (/Users/kevin/Dev/wheatt/node_modules/router/lib/route.js:142:13)
[backend:dev]       at Route.dispatch (/Users/kevin/Dev/wheatt/node_modules/router/lib/route.js:107:3)
[backend:dev]       at handle (/Users/kevin/Dev/wheatt/node_modules/router/index.js:421:11)
[backend:dev]       at Layer.handleRequest (/Users/kevin/Dev/wheatt/node_modules/router/lib/layer.js:101:15)
[backend:dev]       at /Users/kevin/Dev/wheatt/node_modules/router/index.js:288:22
[backend:dev]       at processParams (/Users/kevin/Dev/wheatt/node_modules/router/index.js:568:12)
[backend:dev]       at next (/Users/kevin/Dev/wheatt/node_modules/router/index.js:282:5)
[backend:dev] }

My main problem, once I figured out that “exn” is the type that worked for both was that I didn’t know how to switch on these exceptions without raising again. And I don’t think anywhere outside the express.js world would anybody be passing exceptions as a argument, its not natural.

I’m not sure to follow, but once you’ve defined that type err = exn, all you have to do is pattern match on it, doesn’t this work:

switch err {
| Not_found => Console.error("not found")
| Js.Exn.Error(e) => Console.error(e) // handle your JS exception here
| otherError => Console.error2("unexpected error", otherError)
})

I hope I didn’t screw something up, but it looks like

This

let handleError = (express: t) => {
  express->useError((err, _req, res, _next) => {
    let uuid = randomUUID()
    // switch err {
    // | Js.Exn.Error(obj) => Console.log4(uuid, Exn.name(obj), Exn.message(obj), Exn.stack(obj))
    // | _ => Console.log3(uuid, "rescript exn mystery meat", err)
    // }
    try {
      raise(err)
    } catch {
    | Js.Exn.Error(obj) => Console.log4(uuid, Exn.name(obj), Exn.message(obj), Exn.stack(obj))
    | _ => Console.log3(uuid, "rescript exn mystery meat", err)
    }
    res->json({"error-logged": uuid})
  })
}

does

[backend:dev] 7a6ed568-0b88-4333-a75a-657f5b9b64d1 Error Validation error, password is None, {"email":"me@example.com","passwordXXXXX":"the password"} Error: Validation error, password is None, {"email":"me@example.com","passwordXXXXX":"the password"}

flipping the commented code to

let handleError = (express: t) => {
  express->useError((err, _req, res, _next) => {
    let uuid = randomUUID()
    switch err {
    | Js.Exn.Error(obj) => Console.log4(uuid, Exn.name(obj), Exn.message(obj), Exn.stack(obj))
    | _ => Console.log3(uuid, "rescript exn mystery meat", err)
    }
    // try {
    //   raise(err)
    // } catch {
    // | Js.Exn.Error(obj) => Console.log4(uuid, Exn.name(obj), Exn.message(obj), Exn.stack(obj))
    // | _ => Console.log3(uuid, "rescript exn mystery meat", err)
    // }
    res->json({"error-logged": uuid})
  })
}

does


[backend:dev] 2111fccb-94e5-43b4-b3f0-b0645868f15c rescript exn mystery meat Error: Validation error, password is None, {"email":"me@example.com","passwordXXXXX":"the password"}

looks like just switching on err doesn’t match Js.Exn.Error unless you reraise it first.

Doesn’t a try/catch in rescript have some secrete sauce syntax, like “exception” in the sample below?

switch list{1, 2, 3}->List.getExn(4) {
| item => Js.log(item)
| exception Not_found => Js.log("No such item found!")
}