Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Getting data out of an Error, easier classifying unknown values, "Type" module thoughts #102

Open
jmagaram opened this issue Mar 16, 2023 · 12 comments

Comments

@jmagaram
Copy link
Contributor

jmagaram commented Mar 16, 2023

I'm trying to do something simple. I caught a Firebase exception and want to access the code property that is a string. Firebase errors have code properties with contents like "auth/email-password-invalid". How can I do that? Here is one awkward way. Is there a simpler way?

@get_index external getErrorProperty: (Error.t, string) => option<'a> = ""

let toString1 = i =>
  switch i->Type.Classify.classify {
  | String(s) => Some(s)
  | _ => None
  }

let printError = (err: Error.t) =>
  err
  ->getErrorProperty("code")
  ->toString1
  ->Option.getWithDefault("No code found in the exception")
  ->Console.log

I think it would be useful to access arbitrary properties on Error.t just like I can on Object.t. I also want a much easier way to work with those values and not have to switch on Type.Classify.classify.

First idea is to add an Error.get function just like we have Object.get. This would be very convenient.

Second idea is to add convenience methods for classify like this. All these do is wrap the classify function with a Switch.

  let classify: 'a => classified
  let toString: 'a => option<string>
  let toFloat: 'a => option<float>

The third idea is to rename the Type module to Unknown and have an unknown type. When we have functions that return some unknown thing we could label them explicitly as unknown and the developer will then know they can use the Unknown module to classify and make sense of it. The rescript-struct module defines as unknown type that is used for serialization output. https://github.com/DZakh/rescript-struct#sserializewith TypeScript has an unknown type. The Object.get function returns an option<'a> which is too permissive because you can Option.map it and start treating it like something it is not and get a runtime error. So Object.get should return an option<unknown> and force the developer to safely inspect and classify it.

Finally and some random thoughts on the Type module.

  • It won't classify a bigint.
  • The classify symbol doesn't result in a Symbol.t - it is a Type.Classify.symbol
  • The classification method uses toString. Why not typeof? Wouldn't that be more efficient?
  • For a tiny module it is kind of annoying having a Classify sub-module just to hold the classify function. Couldn't this all be flattened out?
@jmagaram
Copy link
Contributor Author

jmagaram commented Mar 16, 2023

I built some helper functions and here is how I use it to get the details I want from an exception...

    } catch {
    | _ as exn => {
        let isUserNotFound =
          exn
          ->Error.fromException
          ->Option.flatMap(ErrorExtras.get(_, "code"))
          ->Option.flatMap(Unknown.toString)
          ->Option.filter(i => i === "auth/user-not-found")
          ->Option.isSome

@jmagaram
Copy link
Contributor Author

Some experimenting...

  {"favorites": null}
  ->Object.get("favorites")
  ->Option.map(i => i->Array.length)
  ->Option.getWithDefault(0)
  ->Console.log // Run-time error of "Cannot read properties of null"

  {"count": null}
  ->Object.get("count")
  ->Option.map(i => i + 6)
  ->Option.getWithDefault(0)
  ->Console.log // Outputs 6, null + 6 = 6

@zth
Copy link
Collaborator

zth commented Mar 21, 2023

What about an Exn.unsafeGet? Like you proposed first. let unsafeGet: (Exn.t, string) => option<'a>.

Then people can decide themselves how safe they want to play it after using that.

@jmagaram
Copy link
Contributor Author

I'm thinking this over. Is a JavaScript exception ALWAYS an object? If I throw a primitive does it get wrapped?

@glennsl
Copy link
Contributor

glennsl commented Mar 21, 2023

No, you can throw anything in JS. Though everything in JS is an object in the OO sense.

Also, interestingly, I believe the stack trace is recorded when the Error object is created, not when it's thrown.

@glennsl
Copy link
Contributor

glennsl commented Mar 21, 2023

What about an Exn.unsafeGet? Like you proposed first. let unsafeGet: (Exn.t, string) => option<'a>.

Then people can decide themselves how safe they want to play it after using that.

I think the naming convention would be getUnsafe. Otherwise I agree 🙂

@jmagaram
Copy link
Contributor Author

This situation with getting a custom value out of exn or Error or dealing with a getter on Object is not ideal. (a) sometimes we don't have the getter and are considering adding getUnsafe. And (b) sometimes we get an 'a which is like a TypeScript any but that isn't true. I can treat it as a string or array and get a runtime exception or unpredictable results.

I'd like to prototype a Unknown module and unknown type. This would be the Type module renamed with classify and other functions like toString, toStringUnsafe. And this could be used to pull data out of Error or exn and safely parse values retrieved from Object.get.

Are you interested in seeing my ideas on this?

@glennsl
Copy link
Contributor

glennsl commented Mar 21, 2023

See my comment here: #108 (review)

@cristianoc
Copy link
Contributor

Btw type unknown exists already.
It does not have any instances.
It's currently used as the payload type of JsError

@woeps
Copy link

woeps commented May 17, 2023

I'm trying to do something simple. I caught a Firebase exception and want to access the code property that is a string. Firebase errors have code properties with contents like "auth/email-password-invalid". How can I do that? Here is one awkward way. Is there a simpler way?

We have a similar usecase when interacting with aws-sdk which occasionally may return Error objects with some additional properties than it's original prototype.
The pattern we currently decided to use is to type the specific error as a record and provide an identity function to "cast" any `Js.Exn.t´ to this record type.

module CustomError = {
  type t = { message: string, code: string, otherCustomProperty: bool }
  external ofJsExn: Js.Exn.t => t = "%identity"
}

// somewhere else
switch await someAsyncStuff {
  | result => Js.Console.log(result)
  | exception Js.Exn.Error(err) =>
      let customErr = err->CustomError.ofJsExn
      customErr.code->Js.Console.log
}

This is obviously unsafe to do. But any way to get some property of an object (in js representation) after the fact, that it was returned as an "unspecific" type would be.

The only safe way for us, I see, is to actually not use the promise api of aws-sdk, but the callback api and type the callback arguments with something alike CustomError.t. (Or catch any raised promise early and resolve it as an result type, so we only need to handle resolved promises down the road.)

@DZakh
Copy link
Contributor

DZakh commented Jun 30, 2023

It won't classify a bigint.

Is solved in #146

@DZakh
Copy link
Contributor

DZakh commented Jun 30, 2023

The classify symbol doesn't result in a Symbol.t - it is a Type.Classify.symbol

Is solved in #145

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants