• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 8/22/18

Assess the app call stack after a crash

Log in or subscribe for free to enjoy all this course has to offer!

We've got our hands on the project, so open it in Xcode.

What now?

Well, first things first - check out how it works as-is... Click RUN! Keep your fingers crossed and hope for the best. A usual simulator should come up and display the main view.

Oops… that didn't happen! Instead, we got this:

Tic-tac-toe CRASHED!
Tic-tac-toe CRASHED!

Let's look at the console:

Mysterious output in console
Mysterious output in console

That's called a crash!

That was unexpected. At this point, we can't even check what the app is about. But not to get discouraged - we are about to learn how to deal with it! 💪

Xcode is a sophisticated tool that's designed to assist developers with various tasks, including identifying what's wrong with the code.

Call stack

When an app crashes, Xcode presents us with additional information regarding the issue by displaying what’s called a Call-Stack in the console.

A call-stack is used to store the call sequence of all the functions of a program. This mechanism makes it possible to keep track of the functions that are called during the app execution, in order to be able to resume the order of instructions when a function is completed.

Let's use a small example to illustrate this:

  • Imagine that you have a function named  getTemperature  that returns the outside temperature.

  • This function uses another getTemperatureFromSensor  function, which allows you to query the thermometer.

  • Finally, the function  getTemperatureFromSensor  uses a last function named  convertFahrenheitToCelsius  that converts a temperature initially in degrees Fahrenheit to degrees Celsius.

The corresponding simplified code would look like this:

func  getTemperature() -> Int {
  return getTemperatureFromSensor()
}

func getTemperatureFromSensor() -> Int {
  let fahrenheitTemperature = sensor.getTemperature()
  return convertFarenheitToCelsius(farenheit:  farenheitTemperature)
}

func convertFahrenheitToCelsius(fahrenheit: Int) -> Int{
  return (fahrenheit - 32) * 5 / 9;
}

When we call the  getTemperature  function, it is automatically added to the stack. The stack therefore contains:

Stack

getTemperature

Then, within the  getTemperature  function, we call  getTemperatureFromSensor. It's also added to the stack. Now stack contains two items:

Stack

getTemperatureFromSensor

getTemperature

And finally, when  getTemperatureFromSensor  functions  convertFahrenheitToCelsius  function, the 3rd item is added to the stack:

Stack

convertFahrenheitToCelsius

getTemperatureFromSensor

getTemperature

imagine a call-stack like a pile of paper documents on a desk. When we call a function, we add a document on top of the pile. When the function is completed, we remove the corresponding document from the pile! This ensures the correct order of instructions in the app.

The items constantly get added and removed from the stack. The items that got placed into the stack earlier are referred to as "lower layers" and the latest as "higher layers."

Analyzing the stack

When a crash happens, the first thing we need to do is analyze the call-stack. That's where the error that caused the crash is described.

Now let's look at our stack. It contains 46 calls from the lowest layers of our program to the highest layers. Here we need to identify the lines that correspond to our code and we can ignore the rest for the moment.

Let's highlight different parts of the output:

Call-stack sections
Call-stack sections

We can identify 3 groups here:

Stack

Error details - an explanation of an error that caused a crash

ViewController related functions that led to the crash

The log of the beginning of the call-stack before the error causing view controller functions

What interests us at the moment is the green section - a group of logs related to the ViewController functions that led to the crash:

7 TicTacToe             0x000000010676ba07_T09TicTacToe14ViewControllerC10clearBoardyyF+583
8 TicTacToe             0x00000001067683ac_T09TicTacToe14ViewControllerC11viewDidLoadyyF+108
9 TicTacToe             0x0000000106768444_T09TicTacToe14ViewControllerC11viewDidLoadyyFTo+36

| Slack  ... || clearBoard || viewDidLoad || viewDidLoad || ... |

So the error occurred during execution of the  clearBoard  method of the ViewController class.

So, what's wrong with this code?

To identify the details of the error, we need to refer to the note at the very top of the console, above the green section:

2018-04-23 11:02:42.533527+0200 TicTacToe[18950:953130] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSFrozenArrayM objectAtIndex:]: index 9 beyond bounds [0 .. 8]'

It seems to mean that we're trying to access an element in an array at index 9, but the largest available index is 8. We'll need to do a little investigation to see why this is so!

You may wonder, why did the app crash?

When you made errors previously, Xcode was able to notify you right away! This is true, but only for certain type of errors.

Compilation vs execution

There are 2 processes associated with preparation of an app:

  • compilation

  • and execution.

After the code is written in a programming language, it's then compiled into an executable version of an application and, finally, when application is run, it gets executed:

Writing code ➡️ Compiling into an executable ➡️ Executing

Compilation

What's compilation? 🤔

Compilation is the process of translating the code from the original programming language into machine code that a computer can understand. The compilation is performed by software that's called a compiler.

A result of the compilation process is a piece of code in executable format, called an executable. In order for it to be completed, we must resolve all the errors!

Execution

The final code after compilation is now in a machine-understandable format ready to be executed on a target platform - iOS in our case.

When is this code executed?

The executable is executed when we launch our app. On the iOS platform, it happens when we tap on the app icon on the home screen of a mobile device.

Let's illustrate the full process:

Writing -> Compilation -> Execution
Writing ➡️ Compilation ➡️ Execution

Great, but what's the point of compilation? Can't iOS just execute from the original language?

The compilation makes it possible to generate machine code - the most primitive commands that need to be followed. Which, in turn, guarantees the fastest execution.

Imagine if you were to cook a meal and needed to produce the most precise result in the fastest way possible. Think of a fast food restaurant where all the ingredients are measured and packaged in the most optimal way, versus a boutique restaurant where meal creators follow a much slower but more engaging and creative process.

Machines, like honey badgers, they don't care...

They don't...
They just don't...

During the execution, new errors may occur that are not related to correctness of the code, therefore, could not be detected at compilation.

Two types of errors

Each phase of app processing comes with its own separate set of potential errors:

  • Compilation errors 

  • and Run-time errors.

Compilation errors

Compilation errors can be detected by Xcode while generating an executable - during translation of your code from swift to machine code. In fact, Xcode does it in live mode, as you type, and shows you warnings and errors.

These errors can be syntactic - incorrect spelling - or semantic - such as use of forbidden words, non-interpretable code, incorrect types, etc.

The lines of code that correspond to compilation errors are highlighted in red.

Run-time errors

The runtime errors occur during the execution process - after the app is launched. They 'appear' only during the execution process, and there's no way to detect them during compilation.

When a runtime error occurs, it causes an application crash. This kind of error is typically related to the logic of the code, such as accessing data that doesn't exist or that exists in a different format, or performing an unsupported action, etc.

Some of this errors can be easily reproduced - they happen consistently. Others occur 'occasionally' - those are harder to fix as they are typically harder to reproduce. Occasional errors are usually related to specific conditions in the code. Here's an example of a ninja-error in pseudo-code: 

if week-day run CORRECT CODE
else run INCORRECT CODE

In the example above, if you are working on your project only Monday to Friday and get all the crashes reported on weekends, it won't be evident at first why your app is crashing.

Once we manage to reproduce a crash, thankfully we have the details of a crash in console!  Let's illustrate compilation and execution errors:

Compilation errors vs. Run-time errors
Compilation errors vs. Run-time errors

You already know how to handle compilation errors! And now you need to learn how to deal with run-time errors!

Let's Recap!

  • A call-stack stores the call sequence of methods (or functions) of a program.

  • When an application crashes, the call-stack and the error are displayed in the console. This information helps trace the problem.

  • There are 2 stages of code processing for an app - compilation and execution.

  • Compilation is translation of the code from a programming language to machine code. 

  • Execution is a process of executing instructions of machine code generated after compilation.

  • There are two types of errors associated with the 2 processes: compilation errors and run-time errors.

Example of certificate of achievement
Example of certificate of achievement