I’ve recently started playing around with Elixir. It’s quite nice that Elixir pattern matching errors include the value that failed to match (and line numbers). If this CloudFront request fails:

{:ok, _, _} = AWS.CloudFront.create_invalidation(...)

… then I get this error message:

(MatchError) no match of right hand side value:
    %{body: "<?xml version=\"1.0\"?>\n<ErrorResponse xmlns=\"http://cloudfront.amazonaws.com/doc/2020-05-31/\"><Error><Type>Sender</Type><Code>MalformedInput</Code> \
            <Message>Unexpected list element termination</Message></Error><RequestId>...</RequestId></ErrorResponse>",
      headers: [{"x-amzn-RequestId", "..."},
                {"Content-Type", "text/xml"},
                {"Content-Length", "283"},
                {"Date", "..."},
                {"Connection", "close"}],
      status_code: 400}}}
  (foo 0.1.0) lib/application.ex:66: Foo.Application.run!/0
  (foo 0.1.0) lib/application.ex:13: Foo.Application.start/2
  (kernel 9.2) application_master.erl:293: :application_master.start_it_old/4

Which is wordy–but that’s a good thing! It gives me lots of useful information for debugging. In contrast, in TypeScript I’d probably have manual checks like:

try {
  const result = await cloudFront.create_invalidation(...);
} catch (error) {
  throw Error(`create_invalidation failed: ${error}`);

In the case of AWS, the error string at least contains the error message. But more often than not error.toString() will just be an error message like “request failed” and I’ll need to list out the fields myself:

"message=${error.message} statusCode=${error.statusCode} ..."

… to get useful information.

The browser APIs themselves are not great. For instance, if I evaluate:

(await fetch("/url-that-404s")).toString()

… the result is a terse [object Response], which is not too helpful. If I console.error(error) I might get a debug string, but throw Error("error: ${error}") uses toString().

How do other languages fare? Rust has the experimental assert_matches! macro, which is nice like Elixir in that it prints out a debug string representation of whatever failed to match. OCaml will raise a Match_failure expression but its arguments are just the file name, line, and column (!). IIRC Swift is similar to OCaml.

If the difference sounds trivial, imagine something fails in production and all you have are stderr logs. Without writing any extra Elixir code, I’m already ready to start debugging. Meanwhile I’ve had to go back and add additional logging so many times in TypeScript or Java codebases when it’s turned out that what I was already logging wasn’t enough. Imagine you hit an error 3 hours into a batch job that takes 8 hours to run, and now that you’ve added the print statement you need to wait another 3 hours just to see the results…

A language needs a few features to make this experience work: