logo

Introduction to time travel debugging

profile photo
Filip Hric
Image without caption
Photo by Mohamed Osama on Unsplash
Applications get complex very quickly. Bugs become harder to identify, build times get longer and longer, and issues can only be reproduced with a specific state. Eventually, we run into a situation too complex for print statements and turn to more powerful debugging tools.
Hot reloading, time travel, and step-through debugging are popular options. We throw these phrases around quite often, but what do they mean exactly?
All three are tools to make finding and fixing bugs faster and easier, but they all take a different approach.
  • Hot reloading means swapping, adding, or removing modules while an application runs without a full reload. This can significantly speed up development.
  • Step-through debugging allows you to “break” or suspend application execution. While suspended, you can examine values, add more “breakpoints,” and move forward through the application by either stepping into or over the line of code currently suspended.
  • Time travel debugging allows you to go backward as well as forwards! You can still use a step-through debugger, but now you’ll have even more options for moving around the codebase.
In short, time travel debugging means you can go back in time!

Hot Reloading

What is hot reloading? According to the folks working on React Native:
The idea behind hot reloading is to keep the app running and to inject new versions of the files that you edited at runtime. This way, you don’t lose any of your state which is especially useful if you are tweaking the UI.
This is an evolution of “live reloading,” where any change to a file would instantly reload your application. With hot reloading, we don’t need to reload the entire app; we can just swap out the file you edited. This results in a much faster turnaround time as well as maintaining state. Look at this example of editing a page that’s multiple clicks into your app with live reloading vs. hot module reloading.

Time Travel vs. Step-through

It can be difficult to understand the differences between a time traveling debugger and a browser debugger. Let’s take a look at some example code:
plain text
function createWord() { let word = "Replay"; word = addToWord(word); return changeWord(word); }function addToWord(word) { // This is the bug word = "Recrd and " + word; return word; }function changeWord(word) { word = "Record and Rewind"; return word; }console.log(createWord());
Click here for a live Replit of the above code! And here for a Replay of it running!
Open up your favorite browser and add a breakpoint on the line with word = "Record and Rewind";. It will pause execution with the variable word set to "Record and Rewind." Perfect!
Now you can inspect a local variable like, but you can see by this point it's already too late. There's a typo in the word already. With browser DevTools you can step backward through the call stack, but if you take one step back to createWord But we quickly see the bug is likely down a different code path. It looks like the bug was caused by the addToWord function. Unfortunately, we didn't add a breakpoint on that path, so we'll have to add it and refresh the app to start over again!
With a time traveling debugger, we don’t have to worry about getting the wrong code path! The entire session is recorded so we can add and remove breakpoints at will. Adding a new breakpoint to addToWord after the fact is no problem, and we can rewind execution to that point and spot our bug!

How do we accomplish time travel debugging?

We have two main options when writing applications to allow developers to time travel debug.
  1. We can use immutable data structures and pure functions.
  1. We can record runtime inputs and non-determinism needed to replay the program after the fact.

What are pure functions?

Pure functions are functions that have the following two properties.
  • The function return values are identical for identical arguments (no variation with local static variables, non-local variables, mutable reference arguments, or input streams).
  • The function application has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments, or input/output streams).
For React developers, all this talk about side effects and immutability might remind you of the popular state library Redux! That’s because one of the Three Principles of Redux is that all changes are made with pure functions.
An example of a pure function is:
plain text
functiondouble(number) { return number * number; }
The double function will always return the same value when given the same argument. If you call double(2) it will always return 4.
This means tools like Redux DevTools can simulate time travel, allowing you to step backward and forward through state transitions. To do this, they simply start with the initial state and replay each change up to the one you clicked on.

How can we record an entire application?

The simple answer is…. we can’t. There would simply be too much data to record every single thing that happens during a session. So then, how does Replay work?
In the simplest case, Replay will just record the inputs because everything can be replayed later. If you want to see how many times a function was called, Replay can re-run the program and count the number of function calls. If you are paused on line 10 and want to rewind to line 7, Replay can just re-run the program to line 7. No problem! Learn more about how Replay works here.
Record your application once, and then you can open the Replay and use their step-through debugger instead, which can move backward and forwards!
Related posts
post image
The best way to setup an octokit instance so that it can interact with the API on your behalf.
post image
How to set up a GitHub App that can listen for pull requests and create checks.
Powered by Notaku