[Help wanted] Improving "Somewhere wanted" error messages

We fairly often get reports that the “Somewhere wanted” error messages can be confusing. Therefore, we’ve started looking into how we can improve them. They typically look like this:

This has type: int
Somewhere wanted: string

We’re looking for your help in finding as many situations as possible where this message is confusing. Please post a reproducible example (preferably via the playground) and include why the message is confusing, and if you want/can, a suggestion for how to make it clearer. The most important thing though is to include why you personally find it confusing.

Example

An example (taken from a forum post):

@module("shelljs")
external cd: string => string = "cd"
external exec: string => string = "exec"

let cloneInTemp = (temp: string): string => {
  cd(temp)
  exec("git clone git@github.com:myorg/myrepo.git")
}

For the line saying cd(temp), this yields: “This has type: string. Somewhere wanted: unit”. I find this confusing because I don’t understand what has type string, and why it’s expected to have type unit instead. Nor how to fix it. One easy way to improve it would be to say this instead:

This function call returns: string
But it's expected to return: unit
9 Likes

This would be a fantastic improvement. This is the most common question I have gotten when onboarding new developers to ReScript. Usually it comes up due to the unfamiliarity with everything in ReScript being an expression rather than blocks, i.e. switch, if / else.

Some kind of code blocks support like the rust compiler would be greatly helpful (albeit this is a general thing, not necessarily specific to this error)

https://rescript-lang.org/try?version=v10.1.2&code=AINwhgNgBApgHgFxgJwHaSgMwPbYFxQDOCyAlqgOZQC8UARDtnQFDxJoaHYC2MAYgFdUAYwSlsqAkNIIaAPiIlyVWnS69BIsRJbMIMWQCMwyGlAAUASnlQA3syhEA7jOEALLLjsPHUAD70TtjIEAAmdDZWPo4BAPo26vxCouKoUY4AvsxZQA

@val external foo: string = "foo"
external someFunction: unit => string = "someFunction"

let bar = () => {
  switch foo {
    | "world" => ()
    | _ => someFunction()
  }
}

This is confusing because it is not immediately apparent where “somewhere wanted” is:

This has type: string, and line 6 has type unit. All values for the expression starting on line 5 must share the same type.
In order to fix this, change line 6 to be a string or this line to be unit.

3 Likes

I’ve seen this confuse people. I believe it’s because the error is showing the generic type 'a rather than e.g. int in this case.

Code

let foo = 1

switch foo {
| Some(x) => print_int(x)
| None => print_int(1)
}

Error

[E] Line 4, column 2:
This pattern matches values of type option<'a>
  but a pattern was expected which matches values of type int

I don’t know what context is available, but ideally I think it’d show:

[E] Line 4, column 2:
-This pattern matches values of type option<'a>
+This pattern matches values of type option<int> 
  but a pattern was expected which matches values of type int

Even better would of course be to provide a more thorough explanation.

Playground: https://rescript-lang.org/try?version=v11.0.0-rc.3&code=DYUwLgBAZg9jEF4IEYBQqDOB3AlmAxgBbRwQDeqAPhAMowC2IAFAB4CUiAfBAA4BOOAHZgA+kLCs2VCADkYgkF14DhY4U2RSAvqiA

1 Like

Right. I guess the type checker stops as it encounters the first type error. Not sure it’s easy to do anything about that, but I’ll have a look.

Perhaps just replacing 'a with ... would make it clearer? I.e.

[E] Line 4, column 2:
This pattern matches values of type option<...>
  but a pattern was expected which matches values of type int
3 Likes

I’ve started diving into this, and I’ve found a bunch of scenarios where I think things can be improved. Here’s a write up of the cases I have so far. I’m looking for your feedback! Please let me know what you think and how we can improve things more.

Note: Before the changes listed below, all of the examples would just say “This has type: x. Somewhere wanted: y”. I’m not going to repeat the old messages for each example, but keep the current state in mind as you read.

I’ve opted to add most things as images here. It’s a bit annoying because you can’t copy the text if you want to suggest changes, but I think it gives the best view of the results since you’ll see the applied highlighting.

General: Somewhere wanted → But it’s expected to have type

I’ve changed the phrase “Somewhere wanted:” to “But it’s expected to have type:”. All ears for feedback on even better phrasing.

Before:

 1 │ /* wrong type in a list */
  2 │ list{1, 2, "Hello"}->ignore
  3 │ 

  This has type: string
  Somewhere wanted: int
  
  You can convert string to int with Belt.Int.fromString.

After:

 1 │ /* wrong type in a list */
  2 │ list{1, 2, "Hello"}->ignore
  3 │ 

  This has type: string
  But it's expected to have type: int
  
  You can convert string to int with Belt.Int.fromString.

Math operators

This is a big one imo. That floats and ints can’t be mixed in calculations, and that floats require their own operators with a trailing dot, seems to always trip people up. I’ve spent some extra time here trying to make things clearer on what’s going on and what to do to fix it:

Adding floats

Adding ints

Using constants

Comparison operators

It’s sometimes hard to figure out why a comparison fails. I’ve tried to add more context and a short explanation anytime you use a comparison operator on something that does not have the same types.

Function arguments

In this scenario, I found it helpful to clarify that it’s the function argument that’s expected to be of another type:

Function call return mismatch

When calling a function, it’s cryptic what it is that has the type that’s wrong, and why it’s wrong. I found this phrasing to clarify things a bit:

If branch type mismatches

This one is hard to understand especially for people coming from dynamic languages - all if branches must return the same type.

If condition mismatch

Similarly to above, coming from languages with concepts like “truthy”, it can be hard to understand that in ReScript all if conditions must be bool.

Switches

Switch branches need to return the same type. This can be difficult to spot in the current error message (as noted above by @illusionalsagacity).

13 Likes

These are all helpful improvements in my opinion :pray: Thank you!

I would suggest adding some additional detail as to why it’s expected to be unit here:

Function calls that return a type other than unit are expected to be assigned to a variable. If you meant to call this function and ignore the result, you can throw away the returned string by assigning to a placeholder variable: let _ = ...

I think the wording is probably subject to various people’s opinions on pure functions, but this seems like a good opportunity to teach the user how not to run into the error again.

As a side-note, maybe consistently including (or not) the In ReScript preamble for the explanations would be a good thing

2 Likes

Love this! I’ve been searching for examples in my codebase, you’ve got a good list so far. Here’s my feedback:

  1. I think it all the errors read better without the In Rescript,

  2. Switch and if branch: I’ve always thought of “expects” as a little confusing, would it work to say something more along "But earlier this switch returned: unit"? This clarifies a little bit why the function expects a specific type

  3. Comparison operators: While it might be a type system limitation, generally two different types will never === true, so maybe it can be phrased in that way - This may be a mistake as the two types will never equal true (TS does a similar thing)

1 Like

For the if-branch mismatch, it would be good to call it “if expression”, not “statement”, right?

2 Likes

Using colors:
In the comparison example the error message shows the wrong type in red and the expected type in yellow. The actual value (100) could also be colorized (yellow) to make it clear, why something was expected.

The same could be applied to the other error messages.

2 Likes

this is tremendous improvement over the current situation.

Nitpicking, I’d also remove all the unnecessary In Rescript and would also phrase for the switch But earlier this switch returned: unit, though I’m not entirely sure that there are no exceptions to the order in which the type-checker works.

It would indeed be great to show in yellow the expression that defines the expected type (whether it is in a switch, and if or a comparison) but I’m really not sure we have access to this piece of information unfortunately.

3 Likes

I’ve made some changes according to your feedback (thanks everyone, keep it coming!) and will upload how they now look soon.

Some of the suggestions are slightly trickier/more invasive to make happen. But, we can do this in stages - start now with these general improvements, set a standard for how error messages should look and how much context they should provide, and so on.

2 Likes

In order to get the switch case right we probably need to say both - a) that the switch is expected to return X, and b) that this is because of how type inference works. Although b) does not always fully make sense imo, because there could be a type annotation earlier in the code that forces what the switch should return, or the branch you’re looking at might be the first one, etc. But I’m confident we can find a good middle ground.

1 Like

The biggest point I have in some cases is the ordering of the “Actual” and “Expected”; specially when functors are involved.

For instance, in ReScript 11.0.0-rc3, the following program:

@@uncurried

module Request = {
  type t = {
    url: string,
    alternatives: array<string>,
  }

  /// NOTICE! Only the 'url' field matters for comparison and hashing.
  module Hasher = Belt.Id.MakeHashable({
    type t = t
    let hash = x => x.url->Hashtbl.hash
    let eq = (a, b) => Pervasives.compare(a.url, b.url) == 0
  })
}

This fails with:

  We've found a bug for you!
  /home/manu/src/.../utils/Network.res:39:40-43:3

  37 ┆ })
  38 ┆ 
  39 ┆ module Hasher = Belt.Id.MakeHashable({
  40 ┆   type t = t
  41 ┆   let hash = x => x.url->Hashtbl.hash
  42 ┆   let eq = (a, b) => Pervasives.compare(a.url, b.url) == 0
  43 ┆ })
  44 ┆ 
  45 ┆ let make = (url: URL.t): t => {

  Signature mismatch:
  ...
  Values do not match:
    let hash: t => int
  is not included in
    let hash: (. t) => int
  /home/manu/src/.../utils/Network.res:
    Expected declaration
  /home/manu/src/.../utils/Network.res:41:9-12:
    Actual declaration

There’s some disconnection between your code the “Actual” and “Expected”. This could be improved if the expected and the actual come together. Something like:

Value doesn't match the expected type.
/home/manu/src/.../utils/Network.res:41:9-12:
     let hash = x => x.url->Hashtbl.hash
has type:
     let hash: (. t) => int
which is not included in the expected type:
     let hash: t => int

Other improvements can be to provide suggestions (a la Rust). But that’s probably harder.

Interfaces and the error reporting around that is something I’ve looked into tackling previously and we’d love to make that better. But, compared to the error messages stated above, it’s low prio given that it’s less common/used.

1 Like

I’ve updated the screenshots of the post, and it’s now ready for another round of feedback. Some of the things I’ve changed:

  • Removed all “In ReScript”
  • Added proper highlighting (yellow) instead of wrapping keywords with ``
  • Removed redundant text (comparison and if branch error message had redundant text)

Two new messages:

Array items

This one is also pretty important for newcomers from JS/TS since they’re used to throwing anything into an array.

Setting record fields

EDIT: Reworded the record field message slightly.

3 Likes

If there’s no more immediate feedback (please ping me if I missed something), we’ll go ahead and clean up + merge this first round of improvements soon.

1 Like

I would add a suggestion to use a tuple for the array error message, just in case they want a list like structure with different types.

3 Likes

Good point! Here’s an updated version, let me know what you think:

2 Likes