Debugging in Elm

We talk about our favorite debugging techniques, and how to make the most of Elm's guarantees when debugging.
May 10, 2021

Elm Debugging techniques

  • Debug.todo
  • Frame then fill in
  • Annotation let bindings
  • Using nonsense names as a step
  • Elm review rule to check for nonsense name

Hardcoded values vs debug.todo

  • Todos don't allow you to get feedback by running your code
  • TDD
  • Fake it till you make it
  • Simplest thing that could possibly work
  • Joël Quenneville's article Classical Reasoning and Debugging
  • Debugging is like pruning a tree


  • Take a walk. Step away from the keyboard when you're grinding on a problem
  • (Short, Self Contained, Correct (Compilable), Example)
  • Create a smaller reproduction of the problem
  • Reduce the variables, you reduce the noise and get more useful feedback
  • Reasoning by analogy from Joël's post
  • Elm debug log browser extension
  • node --inspect
  • elm-test-rs

Debug.log in unit tests


Hello, Jeroen.
Hello, Dillon.
And, well, what are we talking about today?
Today we're talking about debugging apps in Elm.
Debugging makes me think of rubber ducks.
Is it just me?
Uh, no.
I was wondering whether it was a pun on debugging in Elm, something.
Well, ducks might eat bugs, but I don't know their diets too well.
But I do know that rubber ducking is an incredibly useful debugging technique.
I usually use a plush toy.
Oh, that's good.
But because I don't have rubber ducks, but I do have a few plushes.
That comes in handy.
Yeah, like the nice thing with plushes is that even when you can't figure the problem
out with the rubber duck, you can at least hug it, and then it makes you feel better.
So you can't go wrong.
It's a win win.
Yeah, exactly.
I mean, a cat seems like it would be the ideal debugging partner.
If it doesn't leave.
If it doesn't walk away.
That would just be sad.
So basically, you don't need debugging in Elm because if you write it, it works as long
as it compiles, right?
I mean, debugging means bugs, right?
Yeah, it means bugs.
So as long as you get it to compile.
So we should really just talk about how to make an Elm application compile, I think.
Yeah, exactly.
Yeah, this is going to be a very short episode.
So we might as well not do it.
And so see you later, Dillon.
You know, I do think that there is, I don't know if it falls under the same category,
but I do find that there's a certain art to being able to solve type puzzles in Elm, almost
like debugging why your types aren't lining up.
And I'm not sure if like, we should squeeze that into this episode, or if it merits an
episode of its own.
It might be a big enough topic to have its own episode.
But that's an interesting process in Elm as well.
Well let's start with the more vanilla standard debugging.
So when I think about debugging, a word that comes to mind for me is assumptions.
And when I say assumptions, what assumptions can I make?
What assumptions am I making that might be preventing me from seeing what's going on?
Like one of the most common things that happens to me when I'm banging my head against a wall
debugging for a long time is I'm making an assumption and I'm not considering that area.
You know, it's like if you're looking for your keys and you like, looking like the first
place you look, you're like, I always keep it in this drawer.
And you look there and you're like, oh, it's not there.
And then you go looking all throughout the house, you pull up all the cushions on the
couch, you look all over the place.
And then you're like, you know what?
I assumed that I was done looking in that drawer, but did I fully search that drawer?
So you've created this assumption, which has blinded you to one of the obvious things,
which is you check that box.
Obviously it's not in that drawer.
Then you go back and you look in the drawer after searching all over the house and there
it is.
You move over some stuff in your drawer and you see your keys.
And that happens with debugging all the time.
Like you're making that one assumption.
So I think that being, just thinking about it as assumptions, I think is really helpful
because you can then examine your assumptions and at least put them out there.
And I think that's why rubber ducking is very helpful because like if, if a person or a
rubber duck walks up to you and says, Hey, what's going on?
And you have to explain the problem to them, then it forces you to enumerate your assumptions
because then you say, well, I know it's not in the drawer because I checked there and
you're like, wait a minute, but did I fully check there?
So, you know, the equivalent in code would be, I know that this data is getting initialized
correctly because I looked at that code and it gets initialized with the right state.
Wait a minute, but does it get initialized in that right state for this code path?
That makes me think of the confirmation bias.
There's a video by Veritasium on YouTube, which I found to be pretty compelling where
he basically goes to people on the street and asks, I have a rule, I'm going to give
you three numbers and you need to find the rule.
So here are my numbers one, two, four, try to guess the rule.
And people say, well, you double it every time.
And that is not the rule, but you can now guess other numbers and I will tell you whether
they match the rule and you can then try to find other rules to see whether they're matching.
And what seems to happen a lot is that people try to confirm what they were assuming before.
So one, two, four.
So you're doubling the numbers every time.
No, that's not the rule.
Two, four, eight.
Yes, that fits the rule, but that is not doubling the number.
16, 32, 64.
You're trying again and again to validate your initial assumption, but that is not what
you should do.
What you should do is try to ponder, is my assumption correct by saying the opposite
or something else?
So does one, two, three fits?
And in this case, yes, it did.
Which is not doubling the number.
That's a great example.
I love that idea of instead of trying to confirm your assumptions, trying to completely deviate
from your assumptions.
And I remember the first ever computer science lecture in my first ever computer science
class and I will never forget it.
And my teacher said, all right, here's a setup.
You're trying to guess a number between one and 100 and you have however many guesses
and you guess a number and I'll tell you if you're too high, too low or guessed correctly.
So what's your first guess?
And then he was having someone in the audience like make guesses.
And this one student was like 17, 23.
He just wanted to get the right number.
And he was explaining like, well, what happens if you guess 17 and I say that's too low,
what do you now know?
And what information does that illuminate and eliminate?
And so what would be the answer that would give you the most information?
And as people who've learned about binary searching.
Get bisect.
You do a bisect, you try to eliminate as much information.
So you split it in the middle.
That gives you the most information in that situation.
And that is a very real thing that happens in debugging is like if you poke at something,
what will tell you the most information?
That is a really important skill.
When you said it before about trying to make the compiler work or to fix the compilers,
I kind of do that same technique with trying to figure where there's a compiler error.
So if I have a list and one of them is of a different type, but I can't seem to figure
out which one because I'm not looking at the type error sufficiently or because it's not
clear, I tend to remove half of them and see whether problematic ones were in that half.
And then do a bisect in that way.
So we're getting into it.
So here we go.
Let's do it.
So debugging compiler errors, debugging types that don't line up.
Some of the things that I really like to use for figuring out how to solve a type puzzle
are do is a very useful tool.
So we're talking about debug to do before debug log.
[00:07:46] do is, you know, I mean, it's the equivalent of like the TypeScript any type,
except that you can't get it into production code because you can't do lmake dash dash
optimize with do.
So the thing is, it is an extremely helpful tool for testing assumptions.
And the basic idea is you take a, you're using it to ask a question.
You're saying you can say, would any type satisfy the compiler here?
And what that allows you to do is that sort of binary searching that you're talking about
where you just say like, ignore all of this other stuff.
But if I did, you know, pipe this to list dot map, this function, maybe dot with default,
this value, etc, etc.
If I started with a do in this one point, would everything else line up?
Is there is there a possible path where that would line up?
Or in other words, if it doesn't, that tells you that the compiler problem or the type
problem is later down the line.
Exactly, exactly.
You've bisected it into into two categories.
The problem either exists within the debug dot the thing you've you've replaced with
a do, there was a fault there, or everything that comes after is inconsistent.
Because if you if you put a do in the thing that starts a pipeline that goes
through list dot map, some function, maybe that with default, then you know, okay, whether
or not I get the value I'm looking for in the spot, I have the do everything
else is not consistent, because there's no value that would satisfy the compiler as that
starting point.
But if it compiles, then you know, okay, I know that there exists a value that would
satisfy the compiler here.
So the the remaining code appears to be consistent, at least.
So what kind of value could I put here?
And at that point, I really like to use a let with a type annotation, and just extract
I wrote a blog post about this that I sent out on my mailing list, but I should really
publish it on my public facing blog.
I've been publishing them both on my public blog and to my mailing list lately, but my
earlier ones, I didn't maybe I'll go dig that up and publish it.
But but I wrote about a technique that I called frame then fill in kind of like basically
talking about this technique of like, if you're solving a puzzle, you start with the easy
things that you can figure out.
So you start with around the puzzle, if you can find the edge pieces and the corner, there
are four corner pieces.
So if you can find the four corner pieces, now, you have some easy assumptions that you
can start with, and then you can start fitting things into that.
But using like an annotated let binding, and for people who may not know, you can you can
create a let, you know, variable in Elm, and you can put a type annotation above it just
like in a top level value,
which you should do, in my opinion.
But yeah, absolutely.
I mean, I think a lot of people just don't realize that you can do that.
Yeah, notice that too, as well.
And if you're using IntelliJ Elm, it can often help you out with you just hit like option
enter and say add type annotation.
And often it gives you a pretty helpful annotation there.
Yeah, it works generally.
Yeah, it's pretty good.
So that's a really helpful technique.
Also I think people may not know, it took me a while to discover this, that the type
variables you have in your top level type annotation.
So if you have something that says this is a list of a, if you use a lowercase a, if
you say this top level value, my list is a list of a, and then you do a let binding and
you annotate something as a lowercase a, that is now the same a as the top level annotation
So it's sort of bound in that same context.
So that can come in handy.
But so frame then fill in, basically the idea is you keep locking in these things that you
know with type information.
You put like a type annotation, you're like, okay, I know this needs to be this type.
This is consistent if I do this, I put a do here and things line up and they're consistent.
Now I just need to get a value of this type.
And then you start extracting sub problems and saying, well, if I had a value of this
type and a value of this type and a function with this annotation, wouldn't that be helpful?
Write those annotations with dos, and then you start to fill in those sub problems.
So it's almost like taking a puzzle, getting the corner pieces, but then you like get a
sub puzzle out of that and you can get new corner pieces by getting those new sort of
core assumptions that you need to work towards.
[00:12:31] do is a really easy way to hard code values, right?
Because if you say this function just needs to take two arguments and returns a string,
you can just say that this function returns an empty string, but the do will
at least give you one, a reminder to remove it.
And two, it works for more complex values because strings are easy to create, but if
you need to create a user, which could be impossible to create because of opaque types
or stuff like that, do is really helpful in that manner.
That's a very good point.
Also like, so yeah, that's a good distinction between like do and hard coding values,
because they can play a similar role and serve a similar function.
But one thing that hard coding can never do is say, I don't know what the type is here,
but is there any type that would be consistent and work for all these things?
So if you do like, if you call like upper on something, and then you call like
times two on something, you can put do as the value you're applying those transformations
to and it's not internally consistent because there are no values that can both be uppercase
to the string and multiplied by two.
So it's not consistent.
But do can answer the question, are there any values where these things are consistent?
So if you don't know what the type is going to be, do can be helpful.
If you do know what the type is going to be, then sometimes hard coding is the way to go.
But as you say, it doesn't necessarily give you a reminder to remove it.
So sometimes it is helpful to like have kind of standard nonsense names that you use.
So you can find them.
You can use Elm review to find your standard nonsense terms of choice in your in your code
You can totally create a rule that says, hey, any function names, replace me underscore
I often call things like thingy, I'll call it thingy or something.
I tend to go with foobar.
And that's good too.
Some people don't like it.
But for me, at least it's a reminder that I need to rename it.
I think it's very valuable to have like some go to terms that you can quickly scan and
be like, oh, I'm not done with this.
I need to come back and fix something.
We did a workshop together with the Duen and Falco.
Someone else.
And we chose the name nonsense for as a nonsense name.
It's a good nonsense name.
And one thing to keep in mind is that with hard coded values, you can at least run your
application if you have a debug to do it will crash.
And that's actually why I actually tend to prefer hard coded values when possible to
debug to do's.
As a rule of thumb, I like to have debug to do's be extremely short lived to just help
me answer those questions and binary search my compiler errors.
But then as quickly as possible, I want to replace it with hard coded or real values.
So like one example of using like a hard coded value.
This is a debugging technique that I find really helpful.
Sometimes like recently, I was debugging something where there was like, I was tracing the problem
There was like, I mean, it was pretty obvious that there was like a dictionary lookup that
was a miss.
It was not getting value back.
So you know, that was pretty easy to tell because it's like, couldn't find this thing.
You look at the page and that's what you see, right?
At least that was your assumption.
Well, that's true.
But that did seem like a pretty safe assumption that like, it didn't find this thing and you
trace it back here and it's like, it gets that value from a dictionary lookup.
So then you test that assumption, right?
And at that point, I actually draw a lot.
It's actually kind of hard for me to separate my test driven development sort of toolkit
and ideas from my debugging toolkit because really there's so much overlap.
It's almost like the same process for me.
But one of the things I'm trying to do is to get feedback as quickly as possible.
And hard coding is a very useful technique there, right?
So like with test driven development, we talked about this in our test driven development
episode, you can fake it till you make it.
You pass in a hard coded value and you get the test green in the quickest, dirtiest way
you know how.
And then you go and remove the hard coding.
Well, similarly, like you've got this bug, you've got your assumption is a dictionary
lookup is failing.
You can test that assumption by like for a specific case, try to initialize that dictionary
with those specific values.
Or actually, if you want to like if you want to increase your odds of success, start closer
to the actual point of failure, because you don't know what transformation something has
gone through.
So you basically want to get feedback about your assumption as quickly as possible.
If you if you say at the end on this screen, a value is missing, I know that therefore
it must not be getting retrieved from this dictionary.
That's like another assumption, therefore, it must not be getting inserted into this
You actually don't know that.
That's like a bigger leap, because what happens to that dictionary through the course of its
It gets initialized to something get removed at some point, does the dictionary get swapped
out in your model where there's an update and the value gets replaced or re initialized?
You don't know.
So you want to test your assumption and get that feedback as soon as possible.
That's your your first goal.
Yeah, what I would do here is to replace the dict.get or dict.member call by a just value
that makes sense.
And see if I can reproduce the error.
That's a great one.
Yes, right.
That would be a great way to test that assumption.
If you do that, and you still reproduce the error, then this is not the problem.
If it is, then at least your assumption is kind of validated.
Yes, right.
And just to emphasize, this is not code that's going to get committed.
This is temporary code to test your assumption.
So yeah, I think that's a great first step.
You put a just value instead of the value coming from the dictionary lookup.
Does the thing you want show up on the screen?
Okay, good.
Then wind it back a step further.
Instead revert that thing that's taking the dictionary lookup value and putting a hard
code adjust value and instead put a hard coded dictionary.
So what if I had a hard coded dictionary and it had this value with this key?
Would would it be able to look that up?
And if if the answer is yes, it was able to look that up and get the correct value, then
and again, if you're using debug to do's here, it would tell you if the types work, but it
wouldn't allow you to execute it and get that feedback, which is important.
It also wouldn't allow you to run your tests, which is also important.
So but then you work backwards from there and you and then finally, like, okay, well,
at the point where the dictionary is initialized, what if I instead of that hard coded dictionary
I had right at the point of failure when it where does the dictionary look up?
What if I use that hard coded dictionary when I initialize the dictionary?
Now, you know, if my if if if your code now works, you've now shown that the problem is
in the initialization of the dictionary.
So if you fix the dictionary initialization, your code will work.
So now you've narrowed down exactly where the problem is.
You may have created new issues, but that's a different issue.
But at least you have tests for that, right?
Well, yeah, exactly.
And tests are.
Yeah, tests are important.
Like if I when I'm working through a bug, I can't emphasize how much like I essentially
like I've got my full dopamine hit for fixing that issue.
When I get my failing test case, if I reproduce it in a failing unit test, then I've got my
dopamine ready to go.
I understand that.
Because that's the hard part.
If you've got that now you can iterate and test things very rapidly.
And sure, it might still be a tricky problem.
But that tests are not a, you know, a magic bullet.
The way that you're writing your tests matters a lot.
And if you've, you know, not all tests are created equal.
You might have a test that really like is exercising a unit that's not like a clear,
well defined unit.
And at that point, so it really...
You mean if it's integrating multiple pieces?
If it's sort of like an integration test, then it becomes really difficult to like find
the precise failure that you're dealing with and fix that cleanly and get that feedback
because you're not getting this like very precise feedback about where a problem is
So a good unit test suite and getting that, reproducing that failure in a test is incredible.
For all the reasons that we described about testing, like if the problem ever arises again,
then your test will fail.
And yeah, as you said, you get a dopamine rush when you write the test because now you
have something that says red, this is not working, which is kind of what you're expecting.
And when you fix it, you get a big green, which is the dopamine rush of now it's working
as expected.
And I mean, I think that's why I can't separate in my mind my like debugging toolkit from
my test driven development toolkit, because really it's all about feedback.
So like what we're talking about with like this technique of hard coding with putting
a just for the dictionary lookup value just to be like, does this do the right thing?
So it's like writing a failing test and getting it green as quick and dirty as you can.
The reason you do that is just to get that feedback so that you're not wasting time building
on top of faulty assumptions.
So it's all about feedback.
The better your feedback loops are, the more quickly you will be able to test your assumptions
and debugging is all about testing assumptions.
So let's go back to the dict problem that we had before.
Would you really start with inlining just something or would you test your assumption
by adding a debug log to start with?
That's what I tend to do just maybe because I'm too much of a rookie or something, but
I do use debug log a lot.
That's a really interesting question.
I use debug a lot as well.
I don't think that there's anything inherently better or worse about the technique of hard
coding values to get feedback versus debugging.
I mean, really, they're both forms of feedback and sometimes a debug log, if there's like
an intermediary value and you want to figure out, I think I would tend to start with just,
well, if you have no idea what's going on and it could be coming from a lot of places,
then yeah, I mean, pulling up, like doing a debug.log or pulling up the Elm debugger,
like that's a great place to start because the cost of doing that is so low.
So you can just be like, this thing isn't showing up on the screen.
Well, what does the model look like and what do I expect it to look like?
And maybe like if I have a known point of code before, if I get stash or go back to
a previous commit, what did it look like then?
Those are really interesting questions and I don't think that there's inherently like
a better option between those two.
I think they're both great ways to get feedback.
I think debug.log is much easier to set up because like if you take the example of the
dictionary again, it is much easier to inspect whether the dictionary was empty or whether
you got something from it rather than constructing a value, which might be very complex or impossible
to create.
Yes, that's right.
I think that's a good way to put it.
And I mean, there is a real advantage to like the hard coding in that you can actually prove
that it works by seeing it working.
You can actually say like, I had this value here, it would work.
So that's extremely valuable to just know that, I mean, basically it's like, it's all
about choosing where to spend your time.
And if you can, there was a, Joelle had an article on the Thoughtbot blog recently about
classical reasoning and debugging.
It's a really nice post and he talks about this analogy of looking at debugging as pruning
a tree.
I think that's a really nice way to look at it.
You're basically, you know, because of the nature of trees, the number of paths, you
know, grows exponentially.
And so if you can cut off one of those branches and stop examining it, prune that tree, then
the gains are huge.
So that's really your goal.
And if you can, if you can debug.log, it helps you figure out which branches to examine or
where to focus your efforts.
If you can put a hard coded value and prove that it works, if I do this thing, because
I can actually run it and see it working with this hard coded thing.
Now you've actually pruned a branch.
So I would say that those are, just be aware of the purpose that those two different techniques
Kind of like with the guess a number from one to a hundred.
Like if you say just 23 and then it happens to be the right number.
But if it isn't, then good luck.
And a debug.log can definitely help give you a clue because at a certain point, I mean,
kind of like your Veritasium example, like you want, you need to get your bearings of
like, where do I even start looking and just printing out a bunch of information or inspecting
the model is a good place to sort of pick where to start.
If you need to pick where to start, then just sort of like looking at a bunch of information
and seeing if you notice anything interesting, can sort of get your brain thinking a certain
It may also get you thinking with some tunnel vision and getting some confirmation bias.
So you, you, you know, that's why you want to validate your assumptions.
Once you sort of get a place to start and make sure you are explicit about your assumptions,
lay them out, tell a rubber duck or a plush toy about them or a cat taking a walk.
Let's talk about breaks.
Cause that's, that's a really good one.
I think breaks are definitely underrated as a debugging technique.
And walking in particular is really good for like getting your mind off of like just grinding
on a problem, which if you are, um, if you are grinding on a problem, then be aware that
you, you won't want to tear yourself away from the code because you're like, I want
to fix the problem.
Your brain is in that mode.
And often that's exactly the time when you should get away from the code, get away from
the keyboard and take a break because you're going further down that tunnel vision where
you're like, I just want to see this thing through.
You're burning yourself out and just step away from the keyboard for a little bit.
I'm thinking of cases where I would want to, to move away from it because it is very complex
and like, especially if you're dealing with, um, a lot of mutating code where things happen
and impact other code and you have to create a mental model of everything, which is very
complex and you want to keep, um, in that state.
But that also probably means that you have a problem that your, your system is too complex.
Not that you need to resolve it now.
You don't have, you haven't created a small enough system in which to reason about.
So we often talk about SSCCEs in L, which, ah, what does it mean again?
Isn't there like a website that you can point people to?
[00:29:11], short self contained correct example.
Did you also learn that word from Elm?
Um, so if you can create a smaller example, maybe not minimal, but at least small and
well, that makes it much easier to know where the problem is exactly.
And often by doing this, you have found where the problem is because you are kind of pruning
that tree while saying, oh, well this information that actually doesn't impact the, this code
doesn't impact the results.
I'm still getting the bug.
Oh, but when I change this, then bug is resolved.
So this is part of the problem.
Right, right.
So yeah, absolutely.
Because it's really, I mean, again, I keep coming back to it cause I think this is just
like such a fundamental thing about programming that it applies universally, but it's about
And when you reduce down the problem to a simplified version of the problem, it allows
you to, you're reducing the variables.
And that means when you get feedback, it there's less noise to that feedback.
So you're getting better, faster feedback.
I'm thinking if you managed to resolve it to SSCCE, you fix it and then you still have
the problem.
Then you have actually found two bugs.
Yeah, Joël also talks about, he refers to this as reasoning by analogy in his blog post
here, which is, I like the way that he breaks down, you know, these different reasoning
techniques and how they applied in the context of coding.
And so with like debug logging and using like the interactive Elm debugger, like are there
any specific techniques that you find helpful there?
Not specifically.
I would attempt to try to pinpoint where I need to put my debug log and try to get the
most precise information because otherwise I get that big block of text.
Like if I want to know whether a digs gets is a hit or a miss, I debug log that instead
of the key and the digs.
But when I know that I actually need to look at those, then I do a debug log on both.
And actually when you have a big debug log block of text, there's one extension that
is very useful that was made by Thomas Latal, also known as Krakelyn, which I probably pronounce
better, which is an extension that you can add to basically any browser, which when you
turn it on, you can have a nicer view of the debug log, like actually interactive where
you can open a record to see the fields in it.
You can open a list to see the elements in it and so on.
It feels a lot like if you do like console log of a JavaScript object in the browser
and you can kind of inspect and expand pieces of it.
But nicer with colors.
So instead of a big block of text, that is very useful.
I actually am a bit sad because most of the debugging I do, I feel like I can do it in
Elm review.
So it's done with Elm test and I can't use this.
This is so annoying.
You actually probably could if you use node dash dash inspect.
So with node dash dash inspect, you can actually execute because you know, Elm review is running
in a node runtime, not like a Chrome browser runtime.
I was actually thinking with Elm test, but yeah, otherwise, yeah.
Oh yeah.
With Elm test, true.
It's not going to give you, it's not going to give you that.
I mean, unless you pulled down the Elm test runner code and did node inspect with that,
but yeah.
But do give me the information.
I'm still interested.
It can be helpful.
Like if you do node dash dash inspect and you're running your node program, which happens
to execute some Elm code, then you can, you get your console log messages in a Chrome
or whatever window and you just like connect to this node session and you can like analyze
the memory and see everything's going on as if it were running in Chrome.
That's quite handy.
So for debug logging, there are a lot of different techniques that I find myself using.
Like one technique I use often is just, oh, and I wanted to mention one other thing, which
is Matthew Pittenberg's.
I don't know how to pronounce his name properly.
You pronounce it better.
Matthew Pittenberg.
There you go.
That's what I meant to say.
I mean, French names can always be pronounced in different ways, especially last names.
Well then I don't feel too bad.
But yeah, he has this Elm test RS tool.
It's like a rust test driver for Elm unit tests, which is just, you know, drop in replacement
for the Elm test CLI.
And one of the main features that he has for that is that he like captures the debug log
output for specific tests.
So a technique that I use quite often is I will use debug.logging in tandem with Elm
And because that's a really great fast feedback mechanism, I don't need to like run through
the steps in the browser and then try to reproduce the case and make sure I'm reproducing it
the same way every time and context shift between reproducing and adding log statements,
But so that's pretty cool.
In Elm test RS, it captures the log output and associates it with an individual test
I do have to say, though, in practice, what I tend to do anyway is I tend to add an only
yeah, on a specific test anyway.
Yeah, because you would still have a lot of debug logs for all the other tests.
That's right.
Even though you're associating the log output with a specific test run instead of just printing
the logs with it without a specific ordering, it's still a lot of noise.
And it's nice to reduce down the noise in a lot of cases and say like, I know I'm reproducing
the issue here.
Give me log output to help me understand what's going on.
Yeah, I tend to use test.only also quite a lot.
Yeah, only is very helpful.
And you can use only on a specific test case or on a describe.
I tend to just use it on a specific test case in practice most of the time, at least when
I'm debugging.
Yeah, totally.
I tend to do it on describe sometimes when I'm working on stuff.
But then when I add it to a single test underneath it, there's an issue where it still runs the
whole describe.
I mean, if you have multiple onlys, it doesn't know which only you want.
So I think it does them all.
If you do an only inside of a describe.only.
Yeah, that's true.
Then I would argue you need to only run that test, but no, it doesn't.
That's reasonable.
Yeah, that's reasonable.
So another logging technique that I like to use is I'll run an only with a specific test
case and again, like one of the most useful tips is just try to get your red failing unit
If you can do that, you're in really good shape.
But I also like to, often I will just put like a debug log so you can do underscore
in a let binding, you can do underscore equals debug.log.
And that's important because Elm will not evaluate the debug log statement if you just
say foo equals debug.log string 123.
So you need to do it as underscore and then it will evaluate that whether or not it needs
it for any of the other values.
Just as a side note, if you do not see your debug log, don't forget to add two arguments.
That's another yes.
The description between quotes and then the value that you're trying to show.
And if you're not showing, not trying to show anything, just trying to see whether the code
passed through this path, then add a unit or something.
Yes, exactly.
That would be an interesting Elm review rule.
I don't know if there is one for that, but there isn't.
Yeah, that would be a, that would probably save some people.
Yeah, it's already reporting that, hey, you should not use debug log.
So I don't think people will actually look at it.
That's fair.
That's a good point.
But yeah, so if you, so I sometimes find myself trying to understand which code path it's
taking, like in a case statement or an if, if expression or whatever.
So what I'll often do there is I'll put an underscore equals debug.log, like a little
let in each branch of the if or case expression.
And then I'll just put, one, two, three for each of the log statements.
And then I know which branch it's going down.
Don't forget to always give a reasonable description to know that, to make it easy to find the
debug log again.
So Oh, I found debug log two.
Oh wait, where did I put that one?
So we usually do something like before this function call and after this function call,
something like that, or one, two, three, four.
That's less important if you are running it with a reproducible unit test, because if
you're like, where is this log coming from?
Then you find all the places you're logging that and change one of them.
Whereas if you're like, if this is a very hard to reproduce bug and you have, and you
can only reproduce it one out of 10 times, cause you don't know exactly what you did
clicking through these steps to reproduce it.
Then you want to be really sure that you can get all the logging information in that one
So you have to understand through which paths the code is going through.
Yeah, exactly.
That's, that's another kind of pruning strategy to help you identify which branches you can
stop examining and which ones you can focus your efforts on.
So how often do you use the Elm browser debugger?
So the one that comes built in with the, with Elm, it looks almost, but it's actually part
of Elm browser and that it appears in the bottom left, bottom right corner.
I use it, I actually use it fairly often to just, um, inspect what's going on with the
I don't, and I mean, it's used, it's interesting to see like which messages are coming in and
then which, but often it's enough for me to just know like, what is the current state
or like, or to toggle.
Um, actually I find myself pretty often like clicking back.
So if you like expand a particular part of your model, you can like expand and collapse
portions of it.
It retains that if you click between states that this message came in, this message came
in, you can click back and forth between those states.
So it can be really helpful to like toggle between them and see what changed if you expand
the portion that you're interested in examining.
So you can see exactly how it changed in a particular step.
So often that's like easier than the debug.log.
And then when you combine it with some debug logs, you can know why it changed the way
it did.
So you can like find the specific message that triggered it.
And that's another, um, so I was kind of thinking about like, what is unique about debugging
in Elm?
I think that's like, I think that's an interesting topic to talk about, you know, not just for
telling someone why Elm is interesting, but if you're using Elm, how do you make the most
of these cool properties in Elm to, you know, to do, to, to debug more effectively.
And so one of those ways that Elm is really unique is that you have this pinch point of
changing your model and, you know, initiating side effects and that's update and, and an
it, I think of them almost as like the same thing, but you've got, uh, you've, you've
got these messages that you can examine and that's really cool because you can, um, you
can look through and say, is this message getting triggered at the right time or what
happens when this message is happening?
And another unique thing about Elm that you can take advantage of when you're debugging
is types.
So if you can, if you can look at in the update, you know, clause for this message, it's not
triggering any side effects and it's running this function, which only could possibly change
these values that helps you prune the tree and narrow down your search space even more.
Yeah. And also reduce the mental model that you need to keep in your head.
We, we talked about that in our sort of book club about, uh, scaling Elm applications,
Richard Feldman's talk.
And, uh, uh, I think that's a really, that's a really important point is like understanding
how you can narrow down your search space through Elm types.
Also like I think parse don't validate is very relevant here too.
Like if you, if you are passing a maybe down all over the place, you have to constantly
be asking yourself, is this adjust or nothing?
And that's more cognitive load for you.
And I understand that's more code paths for you to consider.
So parse don't validate allows you and refer back to our parse don't validate episode.
It's one of my favorite episodes actually that allows you to, to narrow down information
and basically track your assumptions through types.
That's that's kind of how I think of what parse don't validate allows you to do.
So instead of passing down a maybe all over the place, you want to, um, you want to have
that value that keeps track of the fact that actually if it goes down this code path, it
won't be nothing.
You have a value.
And it will reduce the code.
The amount of code anyway.
So it will look nicer.
Using types, running tests, doing tiny steps also I think is really useful.
Just like all those techniques to not have bugs in the first place or yeah, the less
bugs you have to start with the less debugging you have to do.
So invest in your future, you know?
And again, coming back to the analogy of the pruning the tree, thanks again to Joël for
a really good analogy.
That is, you know, uh, types help you prune the tree and, oh, tiny steps help you prune
the tree.
But if you're taking tiny steps, your search space is smaller because you know, so I mean,
there are a few different cases to consider here, right?
There's like debugging something that you just introduced and then there's debugging
something that a user found in production, right?
And, but if you're debugging something that you just introduced, which, you know, we're
constantly debugging in that regard, right?
You introduce a change, you're like, why is this not working?
If you're taking tiny steps, your search space is a lot smaller.
And also if you want to figure out a compiler error, stash your code and do the same change
in a tiny step, makes the compiler error much easier to read and to understand and to fix.
So back to the Elm debugger, I tend not to use it a lot.
One reason being is that a lot of the debugging I do is for Elm review.
So I don't have access to it, but even in my work application, we don't use it because
we have a lot of events going through all the time because we were showing graphs and
analytics and so we have constant messages coming in and that makes it very hard to find
which step failed and just trying to make the rolling of messages stop.
So one thing I could see us doing, but we haven't done yet is to make it easier for
us to use the debugger by not triggering some events, mostly subscriptions.
If you have a time that every subscription that rolls every frame, for instance, you
will get a lot of messages and the debugger will be unusable.
And it creates performance problems because it's keeping track of all of that state in
memory and explodes.
But if you somehow remove those, like if you remove this subscription, the code might not
work as well, but maybe that's not important for this particular use case or this particular
debugging session.
So you could do yourself a favor by removing it, either by removing it from the code or
by running the code with a certain configuration or adding a shortcut that disables that fetching
of the time.
And by doing that, you will have a lot less noise.
And similarly, what we do do is, that sounded bad, but what we do do, but is not a do do,
is actually kind of invest in our dev tools.
So in all of the pages, we have what we call a dev panel, which we open up with a shortcut,
and then it gives us buttons to put the application in a certain state.
So if you have a list of items that we're showing, like a, for us, it's a dashboard
list because we can have several.
So what would the page look like if you had 100 or 1000?
Would it slow down?
Would it still look okay?
Would the CSS be okay?
So we just have a button that says, add 100 dashboards or empty the dashboards or mock
a request failure so that you can see the error page, the error state.
And when you do all those things, it becomes much easier to test your application in very
specific scenarios.
Even just visually, even if it's only for CSS or styling, being able to easily put your
application into the state where it shows whatever you need to style is very useful.
It's very time saving.
That's great.
That's a great tip.
So that's something that you need to add to your application?
Invest in building your own tools.
Yeah, I think that if you have, I always think about like, what is the sort of macroscopic
effect of improving feedback mechanisms?
I think that you see the quality and level of like innovation around a particular area
directly affected by those types of things.
So that's really cool.
I think if you want to make something really good, invest, I mean, it's like, they have
like chaos monkey and those things at Netflix, right?
So they'll just reproduce an outage in an AWS region or whatever it might be.
And by having those feedback mechanisms, it makes it more resilient and robust because
instead of dealing with those issues when they happen to happen, which is rare, they
can trigger them at any time and they have to build in that resilience to it.
So I think that design tools that bring that feedback out so that you're responding to
it instead of, otherwise it's just going to, it's not going to be front of mind.
It makes me think of fuss tests.
You're not going to test when that integer is minus one, but having a fuss test will
force you to do it.
And it doesn't have the same biases that you have unless you encode your biases subconsciously,
which you probably will to some extent.
It does have some biases, some very weird ones like sometimes, oh yeah, negative numbers.
I like those.
Oh sure.
Well that's, yeah, that's by design, right?
Like it, it, it knows that there is a certain meaning to like an empty list and a list with
one item.
It's like those are, let's not be random about those because those specific areas might be,
let's like be more likely to produce those random values or let's always produce them.
I don't know which it is, but first testing is another interesting one that can sort of
get you out of your cognitive biases.
That's another good, good tip for debugging.
There are almost like two different modes for debugging.
There's like the exploration phase and the fixing phase or, or like narrowing down phase.
So like one is expanding when you're exploring like what is going on, what might be causing
You're trying to expand your options and where to look and one is narrowing within that.
Which we've talked about mostly.
Sometimes people talk about like exploratory testing, which is a type of manual testing.
I I'm not an expert about it.
I don't know very much about it, but I think the general idea is to sort of overcome those
biases by sort of poking at a system instead of saying, here's our test plan.
This is what we test every time we do a release.
It's more how can we overcome our cognitive biases and explore things that might not be
a code path that we've examined before.
I've never, never seen it in action or I'm not sure what it is.
I haven't read a lot about it and I haven't, haven't practiced it myself, but I I know
a lot of sort of automated testing advocates often talk about, you know, ideally almost
all of your testing should be automated, but there's this thing called exploratory testing,
which is really helpful because basically like you're automating the known knowns, not
the unknown unknowns or something like that.
It's to try to flesh those things out, which your automated tests aren't necessarily going
to do a good job doing all the first tests are perhaps an interesting exception.
Is exploratory, is exploratory testing kind of a way to figure where to add tests?
Why should I add automated tests?
I mean, I'm sure, I'm sure you can, let's see how to do exploratory testing, categorize
common types of faults found in past projects, analyze the root cause, find the risk to develop.
There's a whole, there's a whole area here that I need to dig into more, but, but there
could be some interesting concepts to glean there from how to put on the right, the right
lens when you're looking at debugging.
I'll drop a link in case people want to read about it more.
All right.
Anything else you want to add?
I, I didn't bring up my, my mantra of wrap early, unwrap late.
I think that's another good one to mention here.
You know, if you can deal with, you know, deal with possible failures at the periphery,
I mean, Elm bakes in some of those things with like decoders, you know, sort of finding
out about issues when you, you know, when you get the payload rather than when you use
the data.
But I think that that's another really good practice is, you know, you want to enforce
contracts as soon as possible.
You want to use types to represent those.
You want to wrap it in a meaningful type as quickly as possible and, and retain it as
that ideal representation of the data as long as possible until you need to get some, you
know, built in data type that you need to pass as JSON.
You don't want to turn, you know, serialize it prematurely because that makes it harder
to debug and understand what's going on.
Basically any technique that we covered during these other 29 episodes are probably worth
looking into.
Is there anything else that makes Elm unique for debugging?
I guess we didn't talk about like pure functions, but that's pretty, pretty huge.
Just, just the fact that you don't have any spooky action at a distance as we've mentioned
One that sets apart from the others is that there is no debugger between quotes.
The step debugger.
Step by step debugger.
Maybe there will at some point, but there is at a moment.
I've heard people mention that they, they think that would be useful, which I can, I
can sort of see.
Because it's like putting a debug log on the fly.
If you'd put a debug log, you need to know where you want to extract information.
But you can sort of explore freely if you have a stepwise debugger and find something
you weren't necessarily looking for.
Now that said, I think that, I think that you and I would both agree.
Like we would be happier to debug in Elm than in any other language.
We got a pretty good there.
Like even just the fact that there, nothing happens without a message is wonderful.
Like you don't get a message, it will not change.
You get a message, it might change and you know exactly how it has changed.
So yeah, that is in a way a step by step debugger.
The Elm browser one.
Not in the same.
Not the same level of granularity of step, but it is steps.
And yeah, I guess in a way that's like a big bisect already, you can already say.
Yeah, absolutely.
It's only the problem is happening between this step and this step.
And that reduces a lot of the craft already.
And you want to take advantage of the unique characteristics of your application and your
programming language to prune that tree.
So that's why I think it's worth thinking about this question of what's unique about
debugging in Elm because that helps you make assumptions safely.
And so you should double down on using that to step.
Like that's going to make you more effective at debugging your Elm code.
If you're thinking about, okay, what do I know?
Is this as Elm code, is there a message I can look forward to like it's a problem with
Well then what are the relevant messages?
You can start with that because that's an assumption you can make because of Elm, not
because of your domain or the way you've structured your code, which is really cool.
And then with types, that's you can make certain assumptions about what data types are possible.
What can happen here?
Could there be an error or not?
So take advantage of those things and keep them in mind when you're writing your code
and when you're debugging your code and use those to further prune the tree and use those.
And this is really important.
We've mentioned this before, when you find a bug, ideally try to fix it by improving
the types if possible.
If you can prevent the bug in the future through types, do that.
Now that doesn't necessarily have to be your first step in order to get your unit test
In fact, it likely shouldn't be your first step because you want to do the simplest thing
that could possibly work, but go swing back around and make sure it doesn't happen again
by improving the types.
If you find a bug related to types.
There are several ways you can make sure that the problem doesn't happen again, changing
your types so that you make impossible states impossible and other things like that.
You can write a test, which you probably already did by fixing this.
If you had a good test suite.
If you didn't, it's always a reminder that investing your unit test suite is a good thing.
And sometimes when those don't work out, maybe code generation is a good solution.
Elm review rules is a solution.
You have so many techniques at your disposal and you should use the one that is most appropriate.
Basically probably not Elm review.
The closer to the metal you can get, if you can do it through the Elm compiler, then go
for that.
If you can't, then keep going down.
Then code generation, then Elm review.
Try to make it so that it doesn't happen again, because otherwise it will happen again at some
point and you will have to do the same debugging you just did or someone else will.
Even worse.
Basically there's a direct correlation between writing maintainable code and then the techniques
you use to write maintainable code are similar to the thought process you use to narrow down
your search space when you're debugging.
The more you're doing those techniques to write maintainable code, the more you're able
to narrow down your search space when you're debugging and the less you're going to run
into bugs in the first place because your general overall space is more narrow because
there are fewer impossible states that can be represented, et cetera.
Well I think we've covered debugging pretty well.
Yeah, not so much about debugging compiler errors in the end.
I expected more.
Maybe there could be another episode on the horizon for that topic.
That sounds good.
Let us know what you think.
And also thank you to John Doe, Doe spelled D O U G H, for submitting this question.
We neglected to mention in the beginning this is another listener question and we really
appreciate getting these.
It's really fun to get listener questions.
You can submit a question by going to elm dash radio dot com slash question.
Don't forget to leave us a review on Apple Podcasts and tell a friend if you enjoy the
podcast and until next time, have a good one.
Have a good one.