4. Debugging#

A crucial concept that readers have hopefully started to pick up at this point is debugging. Simply put, this is the process by which one fixes code until it accomplishes what it is expected to accomplish. In practice, this can feel like being a detective in a crime movie where you are also the murderer.

There are three types of errors to debug:

  • syntax errors

    • missing parentheses

  • runtime errors

    • overflow error

  • semantic errors

    • “this answer is wrong”

We will explore these one at a time below.

4.1. Syntax errors#

Syntax errors are errors that prevent a compiler or interpreter from understanding the source code and generating a working program.

They are often due to typos, and are the easiest of the errors to fix. Thankfully, they appear less frequently as you master a specific programming language, although it is unreasonably to expect them to go away entirely.

Modular programming (or breaking down the functionality of a larger program into independent and potentially interchangeable modules) helps isolate errors. Throughout this course, this means writing small and readable functions that accomplish only one task, separated into different cells of a Jupyter notebook.

Let’s see some syntax error examples.

fnction mySqrt(a)
    return sqrt(a)
end
syntax: extra token "mySqrt" after end of expression

Stacktrace:
 [1] top-level scope
   @ In[1]:1

Here we see that the stack trace tells us the error is in line 1 of In[1]. Specifically the error is ‘an extra token “mySqrt” after end of expression’. Putting these two clues together, we should see that the word “function” on the first line was incorrectly spelled.

function lots_of_math(n)
    var = 0
    for i = 1:n
        var += sqrt(2*pi*i)*(((i-1)+(i+2))/((n+5)*(5*i))
    end
    return var
end
syntax: missing comma or ) in argument list

Stacktrace:
 [1] top-level scope
   @ In[2]:5

Another syntax error that is quite common (especially if you’re writing complicated mathematical expressions): an uneven amount of parentheses. The stack trace tells us that the error is on line 5 of the above cell, but if we look around line 5, we see that it must be an error carried over from line 4 with the complicated expression.

Here is a helpful tip to zoom in on this issue further. We can track the expression character by character from left to right, counting the number of open parentheses at each character.

sqrt(2*pi*i)*(((i-1)+(i+2))/((n+5)*(5*i))

    1      0 123   2 3   21 23   2 3   21

This count confirms that the syntax error will be solved with an extra ) closing the second half of the expression. The extra ) will ensure our parenthesis count returns to 0.

function more_math()
    i = 10
    var1
    = sqrt(2*pi*i)*(((i-1)+(i+2))/((i+5)*(5*i)))
    println(var1)
    
    i = 20
    var2 = 
    sqrt(2*pi*i)*(((i-1)+(i+2))/
        ((i+5)*(5*i)))
    println(var2)
end
syntax: unexpected "="

Stacktrace:
 [1] top-level scope
   @ In[3]:4

Julia is not expecting line 4 to start with an “=”. This is not allowed and is fixed by combining line 3 and 4 into one line. However, note that you are allowed to end a line with an “=” as on line 8.

When debugging, it is helpful to turn on line numbers by selecting View -> Toggle Line Numbers in a Jupyter notebook.

function array_init(n)
    array = Array{Float64,2}(n,n)
    for i = 1:size(array,1):
        for j = 1:size(array,2)
            array[i,j] = i*j
        end
    end
end
syntax: line break in ":" expression

Stacktrace:
 [1] top-level scope
   @ In[4]:4

A final syntax error to explore. Julia reports that line 4 cannot have a line break in a vectorized expression using “:”. If we explore just around that line number, we’ll see that again the issue was carried over from the previous line. There is an extra : after size(array,1).

4.2. Runtime errors#

Runtime errors are errors that take place while executing a program.

These errors can sometimes be caused by typos, but are often more complex than syntax errors. You should fully read the error message(s) before attempting to fix them. If you are unsure how to interpret an error message you have never seen before, please google the exact expression reported in the error message. You are certainly not the first person to cause this error, and there may be a post online from another user who has already solved your exact issue (such as on StackOverflow.com or discourse.julialang.org).

As always, modular programming will help isolate errors. If you have tested other parts of your program and are confident everything else is correct, then the issue is reduced to somewhere only in a few lines of code.

# Compute the first n terms of the Taylor expansion of e^x centered around x=0
function taylorExpansion(x, n)
    sum = 0
    for i = 1:n
        sum += x^n/factorial(n)
    end
    return sum
end

taylorExpansion(0.5, 25)
OverflowError: 25 is too large to look up in the table; consider using `factorial(big(25))` instead

Stacktrace:
 [1] factorial_lookup
   @ ./combinatorics.jl:19 [inlined]
 [2] factorial
   @ ./combinatorics.jl:27 [inlined]
 [3] taylorExpansion(x::Float64, n::Int64)
   @ Main ./In[5]:5
 [4] top-level scope
   @ In[5]:10

On line 5 of this cell, Julia is reporting that taylorExpansion() is calling factorial() in a separate Julia file which tries to lookup \(25!\), but is instead returning an overflow error. This is because \(25! \approx 10^{25}\) and indeed is much too large to store in the variable sum. A closer inspection of this algorithm shows that we are dividing two large numbers, \(x^n\) and \(n!\), and the algorithm can be rewritten so as to avoid computing such large numbers explicitly, avoiding the overflow error entirely. This process was explored in Section 1.4: For-Loops.

# Confuse the audience with a compicated math expression
function lots_of_math2(n)
    var = 0
    for i = 1:n
        var += sqrt(2*pi*i)(((i-1)+(i+2))/((n+5)*(5*i)))
    end
    return var
end

lots_of_math2(5)
MethodError: objects of type Float64 are not callable
Maybe you forgot to use an operator such as *, ^, %, / etc. ?

Stacktrace:
 [1] lots_of_math2(n::Int64)
   @ Main ./In[6]:5
 [2] top-level scope
   @ In[6]:10

Julia’s error message here has dramatically improved in recent versions. While it reports this as a method error “objects of type Float64 are not callable,” it also explains that this can be caused by forgetting an operator such as *. Indeed, that’s exactly the issue. Comparing this function to lots_of_math1() above, we see the issue is a missing multiplication sign between sqrt(2*pi*i) and the other expression.

The reason it says this is a method error is because Julia is interpreting line 5 exactly like:

var1 = sqrt(2*pi*i)
var2 = ((i-1)+(i+2))/((n+5)*(5*i))
var += var1(var2)

That is to say, Julia is trying to call the function var1() with input var2. This clearly does not make sense since var1 is a floating point number, not a function, hence the error message that Float64s are not callable.

# Print out a comparison between x and y
function compare(x,y)
    if x < y
        relation = "less than"
    elseif x > y
        relation = "greater than"
    end
    println(x, " is ", relation, " ", y)
end

compare(5,5)
UndefVarError: `relation` not defined

Stacktrace:
 [1] compare(x::Int64, y::Int64)
   @ Main ./In[7]:8
 [2] top-level scope
   @ In[7]:11

Apparently, relation is not defined on line 8 above. But isn’t this variable defined on line 4 or 6 directly above? Well actually not at all. Since we are calling compare() with inputs that satisfy \(x=y\), neither conditional is evaluated as true and so relation is still undefined on line 8. This error can be fixed by adding a third condition for when x==y that sets relation = "equal to".

# Initialize a 2d array to hold first m x n integer products
function array_init2(m,n)
    array = Matrix{Float64}(undef,m,n)
    for i = 1:n
        for j = 1:m
            array[i,j] = i*j
        end
    end
    return array
end

array_init2(5,4)
BoundsError: attempt to access 5×4 Matrix{Float64} at index [1, 5]

Stacktrace:
 [1] setindex!
   @ ./array.jl:971 [inlined]
 [2] array_init2(m::Int64, n::Int64)
   @ Main ./In[8]:6
 [3] top-level scope
   @ In[8]:12

A classic out-of-bounds error. You will most likely create this error several times throughout this course, and certainly many more times beyond the course!

To debug this error, we first see that the out-of-bounds error occurs on line 6 where we are accessing array at [i,j]. Therefore, we can quickly determine that either i and j are not correct or the overall size of array is not correct. At this point, we would have to turn on our brains and actually start figuring out the underlying issue. The matrix is in \(\mathbb{R}^{m \times n}\), where \(m\) is the number of columns and \(n\) is the number of rows - that makes sense. But we are looping our i counter over the first n columns and the j counter over the first m rows. There lies the contradiction. Correct code would swap the i and j variables:

for j = 1:n
    for i = 1:m
        array[i,j] = i*j
    end
end

Let’s explore one last runtime error example.

# Divide an input number by 0
function divide_by_zero(input)
    return input/0
end

divide_by_zero("string")
MethodError: no method matching /(::String, ::Int64)

Closest candidates are:
  /(::Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}, ::Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8})
   @ Base int.jl:97
  /(::StridedArray{P}, ::Real) where P<:Dates.Period
   @ Dates /usr/local/julia-1.9.2/share/julia/stdlib/v1.9/Dates/src/deprecated.jl:44
  /(::Union{SparseArrays.AbstractCompressedVector{Tv, Ti}, SubArray{Tv, 1, <:SparseArrays.AbstractSparseMatrixCSC{Tv, Ti}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, false}, SubArray{Tv, 1, <:SparseArrays.AbstractSparseVector{Tv, Ti}, Tuple{Base.Slice{Base.OneTo{Int64}}}, false}} where {Tv, Ti}, ::Number)
   @ SparseArrays /usr/local/julia-1.9.2/share/julia/stdlib/v1.9/SparseArrays/src/sparsevector.jl:1654
  ...


Stacktrace:
 [1] divide_by_zero(input::String)
   @ Main ./In[9]:3
 [2] top-level scope
   @ In[9]:6

This error is somewhat unique to Julia because most other programming languages would report dividing a string by the integer zero as a syntax error. However, Julia specifically allows you to write such general code, but will only catch the error when you try and run it. There are many benefits to this approach, but for now let’s just explore this one possible error. We see that the error reports that on line 7 above, we are calling divide_by_zero() which tries to compute String/Int64, something that has not been programmed.

This error can be fixed by writing that functionality ourselves using the tools we will explore in Section 11: Structs and Objects, or by not trying to divide a string by zero in the first place! This can be enforced by rewriting the function as follows:

# Divide an input number by 0
function divide_by_zero(input::Number) # Only allows an input of type Number
    return input/0
end

divide_by_zero("string")
MethodError: no method matching /(::String, ::Int64)

Closest candidates are:
  /(::Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}, ::Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8})
   @ Base int.jl:97
  /(::StridedArray{P}, ::Real) where P<:Dates.Period
   @ Dates /usr/local/julia-1.9.2/share/julia/stdlib/v1.9/Dates/src/deprecated.jl:44
  /(::Union{SparseArrays.AbstractCompressedVector{Tv, Ti}, SubArray{Tv, 1, <:SparseArrays.AbstractSparseMatrixCSC{Tv, Ti}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, false}, SubArray{Tv, 1, <:SparseArrays.AbstractSparseVector{Tv, Ti}, Tuple{Base.Slice{Base.OneTo{Int64}}}, false}} where {Tv, Ti}, ::Number)
   @ SparseArrays /usr/local/julia-1.9.2/share/julia/stdlib/v1.9/SparseArrays/src/sparsevector.jl:1654
  ...


Stacktrace:
 [1] divide_by_zero(input::String)
   @ Main ./In[9]:3
 [2] top-level scope
   @ In[10]:6

Now the error message makes more sense. There correctly is no function divide_by_zero() that takes in a String as the input.

Note: This fix will not take effect until after you restart the Jupyter kernel so that Julia forgets about the previous function divide_by_zero() which allowed input to be any type.

4.3. Semantic errors#

Semantic errors technically are not errors at all. If there are semantic errors in your program, the computer does not generate any error messages, but the program simply does the wrong thing.

A classic example of a semantic error is an infinite loop or infinite recursion. There may not be an error message reported, but an infinite loop can never be intended design.

Debugging semantic errors might take up the majority of your time while programming, so it is important to set yourself up for success in expectation of these errors. Especially as you work on larger programs and projects, start with small test cases to confirm the logic of your program makes sense. Not to sound like a broken record, but modular programming will certainly help isolate the issues; if you have already tested most of your code successfully and can rely upon it, then you only have to struggle to debug the smaller pieces that fail the tests.

While debugging, use println statements to see what is really going on. A simple tip that goes a long way: assume nothing is correct and slowly start to confirm that what you think is going on inside the program is actually going on by looking at a report of the variables. A more effective method than using print statements is to use a debugger. The IDE Atom allows you debug your code carefully, but Jupyter notebooks unfortunately do not play well with debuggers, so we won’t discuss them too much in this course.

# Returns the sum of the first n integers
function array_sum(n)
    # Initialization
    array = Vector{Float64}(undef,n)
    for i = 1:n-1
        array[i] = i
    end
    
    # Summation
    sum = 0
    for i = 1:n
        sum += array[i]
    end
    return sum
end
array_sum (generic function with 1 method)

First try to debug the above algorithm yourself. What tests would you run? Think small at first! What variables would you like to print in order to better understand what is going on under the hood? Is there a more modular way to rewrite this algorithm in order to easier test the different parts?

# Testing cell for array_sum()

Personally, I would write debugging code as follows:

# Returns the sum of the first n integers
function array_sum(n)
    # Initialization
    array = Vector{Float64}(undef,n)
    for i = 1:n-1
        array[i] = i
    end
    println(array) # TODO: Debugging
    
    # Summation
    sum = 0
    println("Sum before = ", sum) # TODO: Debugging
    for i = 1:n
        sum += array[i]
    end
    println("Sum after = ", sum) # TODO: Debugging
    return sum
end

array_sum(1);
array_sum(2);
array_sum(5);
[6.9310064190874e-310]
Sum before = 0
Sum after = 6.9310064190874e-310
[1.0, 6.93086265245077e-310]
Sum before = 0
Sum after = 1.0
[1.0, 2.0, 3.0, 4.0, 6.9310026199012e-310]
Sum before = 0
Sum after = 10.0

Equipped with all this information, it becomes much more simple to debug the algorithm. We see that there is an issue with initializing the array: the last entry of the array remains undefined even after initialization! That information directly leads us to fix the bounds on the first loop from 1:n-1 to 1:n. Re-running the tests yields the expected answer, and so we can remove the debugging printouts, comfortable that the algorithm is correct.