This is a point of contention in the Julia ecosystem. Many users think the lack of a "good" debugger is the main drawback that prevents them from using Julia.
One common answer is: Yeah, debugging is kind of lousy at this point. Just write your code so you don't need a debugger much. This is pretty much what I do and I find that I don't miss the debugger much at all. Roughly, I work as follows.
- Where possible, I break code into lots of small functions. This is probably anyway a good idea (see Martin's famous "Clean Code" book). Then I write tests for the small functions.
- When I develop custom types, I write test setup functions that make instances of the types that can be used for testing.
- The test functions are placed into self-contained files that can be
included from the REPL. When something goes wrong, it is easy to inspect it there. See testing.
- When something goes wrong in the integrated code that was not found during tests, I start to sprinkle
@asserts into the code. If the code is not performance sensitive (95% of what I write), the
@asserts are never removed, even when the bug is fixed. This gets me closer to the source of the error.
In the vast majority of the cases, this process identifies errors without ever using a debugger. If this fails,
Exfiltrator.jl are helpful. They are a bit like
keyboard statements, but less powerful. The code essentially stops at a breakpoint. The user can inspect the state of the system, but cannot move around the call stack. But that's usually OK because the steps taken before invoking the debugger have gotten me close to the origin of the problem.
Julia offers several debugger options that work in very different ways. The main trade-off is compile time versus run time.
Debuggers that run interpreted code, such as Debugger.jl, compile reasonably fast but run very slowly (about 10 times slower than compiled code). Debuggers that compile debugging features into the code run at near native speed but are either slow to compile (MagneticReadHead) or offer limited features (Infiltrator).
Common options are:
- Debugger.jl: It interprets all code and can therefore offer a complete feature set. But it is very slow for larger projects. Options are available that make Debugger faster, but less powerful. The VS Code plugin gives an IDE experience.
- MagneticReadHead.jl: It compiles debugging features into all code and therefore runs at near native speed. But compile times are often extremely long.
- Infiltrator.jl: It compiles all code, adding only user specified breakpoints (@infiltrate). Compile times and run times are good, but the user can only inspect the local state when a break point is reached. It is not possible to move around the call stack.
- Exfiltrator.jl: Simply exports all local variables at the point of call to Main. The idea is that the entire local environment can be inspected at really no runtime or compilation cost. Simple and effective, but one cannot manipulate objects in the context of the calling module.
There are other options that I don't know much about, such as Juno's debugger.
Stacktraces can be long.
InteractiveErrors.jl can make them easier to navigate.
AbbreviatedStackTraces.jl shows only "own" code and gives much shorter stack traces.