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:
= AWS.CloudFront.create_invalidation(...)
… then I get this error message:
(MatchError) no match of right hand side value:
{:error,
{:unexpected_response,
%{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 catch 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 "/url-that-404s"
… 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:
- pattern matching (not the case in TypeScript)
- match failures print out the value that failed to match (not the case in OCaml/Swift)
- values have useful debug strings by default (not the case in TypeScript); at the very least, it should be easy to automatically generate implementations (e.g. Rust’s
#[derive(Debug)]
)