← Fundamentals
Guided lesson 6 questions · 6 languages

Compile time and runtime

What the compiler knows from the text, and what only the run can know.

The compiler sees only the text. It checks, and sometimes computes, what the text alone fixes: names, types, and constants. Everything that depends on values arriving while the program runs is decided, and checked, only at run time, if at all.

Before you begin

Why this matters

When something goes wrong, ask when it was decided. A mistake in the text can be found before anything runs. A mistake that depends on a value can only appear while the program runs: maybe much later, maybe only for some users, maybe never as a message at all.

What this helps you reason about

  • Say which values in a program the compiler already knows, and which only the run can know.
  • Predict whether a mistake gives a compile error, a run-time error, a warning, or no message at all.
  • Spot code that is compiled but never runs, and settings that are fixed when the program is built.

A TypeScript program meets two times: when the compiler checks it, and when Node.js runs it. This lesson sorts what is known at each time. In TypeScript the answer is unusual: the compiler checks a great deal and computes nothing.

Already know this? The short version
  • tsc checks names and types, then removes the types; it computes nothing. Every value, even const CAPACITY = 4 * 6, is computed by Node.js when its line runs.
  • A const read before its line has run is a run-time ReferenceError. tsc reports it only when the read is outside every function; inside a function called too early, it stays silent.
  • tsc does not check values: a position past the end of an array gives undefined, and 300 / 0 gives Infinity, with no message at either time.
  • An if on a const false is still tested by Node.js, and its body never runs. Changing such a build-time setting means editing the code and building again.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
compile time
The time when the compiler reads and checks the source code, before the program runs. What is decided then is the same in every run.
run time
The time while the program runs. Values that come from a caller, from input, or from earlier steps of the run are known only then.
compiler
A program that checks your code and translates it into another form. The TypeScript compiler checks types, then removes them and produces plain JavaScript.
warning
A message from the compiler about code that is allowed but is probably a mistake. Unlike a compile error, a warning does not stop the build.
build-time setting
A value written in the source code, such as a constant SHOW_DETAILS set to false, that decides what the program does. Changing it means editing the code and building the program again.
dead code
Code that is compiled but can never run, such as the body of an if whose test is always false. Some compilers leave it out of the program.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Compile time

What does the compiler work out before the run?

From source to a running program showed two steps: the compiler checks the text, and then the program runs. This lesson names the two times. Compile time is when the compiler reads and checks the source code. Run time is when the program runs, with real values.

At compile time, the TypeScript compiler, tsc, knows the text: every name, every type, and every number written in the code. It checks them, and then it removes the types. It computes nothing. const CAPACITY = 4 * SHELVES looks fixed, but the multiplication happens at run time, when Node.js reaches that line. Before that line runs, the stepper shows CAPACITY as not initialized yet.

The compilers of some other languages, such as Go and Rust, compute a value like 4 * 6 before the run. TypeScript leaves all computing to the run.

main.ts
const SHELVES = 6;
const CAPACITY = 4 * SHELVES;

console.log("capacity:", CAPACITY);
// capacity: 24

tsc checks names and types and computes nothing; every value, even a const, is computed when its line runs.

Coming from C or Go: A TypeScript const is not a compile-time constant. It is a name that cannot be assigned again, and its value is computed when its line runs.

Precisely: the type of a const

tsc gives SHELVES the type 6, not number: for a const holding a number written in the code, the type is that exact number. tsc uses the type to check other code. The JavaScript it writes still computes 4 * SHELVES at run time.

Experiment

Look for the constant in the stepper

tsc checks the types and leaves every calculation to Node.js. A const is computed when its line runs, like any other line, so there is nothing for the edit to break.

Why it matters In TypeScript, every value is computed at run time, even one made only of numbers in the code.

CAPACITY is made only of numbers written in the code. Predict: will the stepper ever stop on its line, and will it be listed in the variables panel? Then, on that line, replace 4 with countCases() and predict what happens.

function countCases(): number {
  return 4;
}

const SHELVES = 6;
const CAPACITY = 4 * SHELVES;

console.log("cases:", countCases());
console.log("capacity:", CAPACITY);
What to look for

The stepper stops on line 5 and on line 6, where SHELVES and CAPACITY get their values. Before line 6 runs, CAPACITY shows not initialized yet; after it, 24. The stepper can stop twice on one line; that is normal for its debugger. After the edit, the program still compiles and prints the same two lines. The run now enters countCases from line 6, before CAPACITY has a value.

Expected output

cases: 4
capacity: 24

02 / Values

Which values does the compiler check?

The compiler checks what the text fixes: names and types. It does not check values that depend on the run. A parameter is the plainest case. A function must work for any caller, so tsc knows the parameter’s type, never its value.

TypeScript checks little at run time too, because JavaScript gives a value where other languages stop. HALLS below is an array: a list of values in brackets. HALLS[pick] reads the value at position pick, counting from 0. Position 4 does not exist, and the read gives undefined, a value that means “no value”. No message appears at either time.

The order of lines is a run-time fact as well. A const cannot be read before its line has run. Inside a function, tsc cannot know when the function will be called, so it does not check the order.

main.ts
const HALLS = ["Fossils", "Coins", "Maps", "Masks"];

function hallName(pick: number): string {
  return HALLS[pick];
}

console.log(hallName(1)); // Coins
console.log(hallName(4)); // undefined

tsc checks names and types; values, and whether a line has run yet, are known only at run time.

Coming from Java: A position past the end of an array is not an exception in TypeScript. The read gives undefined, and the program continues with it.

Precisely: the order tsc does check

When the read is outside every function, tsc knows the order. console.log(HALL) on line 1 above const HALL on line 2 gives Line 1: Block-scoped variable 'HALL' used before its declaration. The compiler setting noUncheckedIndexedAccess makes tsc treat HALLS[pick] as possibly undefined, so the code must handle a missing value; the stepper does not turn it on.

Experiment

Call a function before its const is ready

tsc checks names and types. Whether a line has already run is a run-time fact, and a function can be called at any time, so tsc does not check the order. Node.js checks it when label reads HALL.

Why it matters A const that a function reads must have run before the function is called. The compiler checks that only for code outside every function.

Predict the output. Then move line 1, const HALL = "Coins";, to the end of the file, and predict: does tsc refuse the program, or does the run stop?

const HALL = "Coins";

function label(objects: number): string {
  return HALL + ": " + objects + " objects";
}

console.log(label(120));
What to look for

Before the edit, HALL shows "Coins" from line 7 on, and the run prints Coins: 120 objects. After the edit, tsc accepts the program. The run calls label from line 6. Inside label, HALL shows not initialized yet, and the run stops with ReferenceError: Cannot access 'HALL' before initialization, at label (line 3).

Expected output

Coins: 120 objects

03 / Warnings

What is a warning?

Between a compile error and silence there is a third answer: a warning. A warning is a message about code that is allowed but is probably a mistake. The program is still built, and it runs.

tsc has almost no warnings: most of its checks are errors, and some are switched off unless you ask for them. With the setting noUnusedLocals, a local name that is never read is an error. Editors also show code that can never run, such as lines after a return, in a faint colour. The stepper uses tsc’s strict mode, which does not include these two checks.

main.ts
function spaceLeft(objects: number): number {
  const extra = 2;
  return 24 - objects;
}
// With noUnusedLocals: error TS6133:
// 'extra' is declared but its value is never read.

A name that is never used is often a sign that the code uses the wrong name; turn on the checks that report it.

04 / Dead code

What happens to code that never runs?

Some values are settings: they decide what the program does. A build-time setting is a value written in the source code, such as a constant SHOW_DETAILS set to false. To change it, you edit the code and build again.

if (SHOW_DETAILS) { … } with SHOW_DETAILS false never runs its body. Code that is compiled but can never run is called dead code. Many compilers leave it out of the program. tsc does not: it writes the if into the JavaScript, and Node.js tests SHOW_DETAILS each time it reaches the if. The answer is always the same. Dead code is still checked, though: a misspelled name inside it is still a compile error.

A setting that must change without a new build has to be read at run time, for example from the command line or from a file. A later lesson, Configuration precedence, builds a program that does that.

main.ts
const SHOW_DETAILS = false;

if (SHOW_DETAILS) {
  console.log("checking Coins");
}
console.log("Coins: 120 objects");

A build-time setting is fixed until the next build; the code behind a setting that is off never runs.

Experiment

Switch off a block before the run

tsc writes the if into the JavaScript unchanged, and Node.js tests SHOW_DETAILS at run time. The answer is still always the same, because nothing can change SHOW_DETAILS while the program runs.

Why it matters A build-time setting changes only when the program is built again. Code behind a setting that is off never runs, so running the program never tests it.

Predict the output, and whether the stepper will stop on the if line. Then change false to true on line 1 and predict again.

const SHOW_DETAILS = false;

function details(objects: number): void {
  console.log("checking", objects, "objects");
}

if (SHOW_DETAILS) {
  details(120);
}
console.log("Coins:", 120, "objects");
What to look for

The stepper stops on line 7, the if, and SHOW_DETAILS shows false. The run skips the body and prints Coins: 120 objects. After the edit, the run stops on line 7 and then on line 8, enters details, and prints checking 120 objects before Coins: 120 objects.

Expected output

Coins: 120 objects

05 / Earlier

Why is an error found earlier cheaper?

A compile error is found by you, before anyone runs the program, and it names the line. A run-time error is found by whoever runs the program with the value that triggers it, perhaps much later. A wrong value with no message at all may never be noticed. So an error found earlier is cheaper.

“It compiled, so it works” is not true. The compiler proved only what the text fixes. Values from callers, from input, and from the clock are tested only by running the program, and only with the values you tried.

You can move some facts into the text, where the compiler checks them. Write a fixed value once, as a constant, and use its name. === asks whether two values are equal and gives true or false. A misspelled name is a compile error. A misspelled text in quotes is just a different text, and the mistake shows only at run time, if at all.

main.ts
const COINS = "Coins";
const hall = "Coins";

// false, and no message
console.log(hall === "Coisn");
// Line 8: Cannot find name 'COISN'.
// Did you mean 'COINS'?
console.log(hall === COISN);

Facts written in the text are checked before the run; facts that arrive at run time are tested only by the values you try.

Coming from JavaScript: tsc catches before the run many mistakes that plain JavaScript shows only while running: misspelled names, wrong types, missing arguments. It still checks no values.

Precisely: a type made of texts

TypeScript can also check a text itself. type Hall = "Fossils" | "Coins" says that a Hall is one of those two texts. Then const h: Hall = "Coisn" is a compile error: Type '"Coisn"' is not assignable to type 'Hall'.

06 / Try it

Can you explain a fresh example?

Predict the output before you reveal the answer. Then sort the names: which values does the compiler know before the run, and which does only the run know?

main.ts
const WINGS = 3;
const ROOMS_PER_WING = 5;
const SHOW_PLAN = false;

function roomsClosed(open: number): number {
  return WINGS * ROOMS_PER_WING - open;
}

if (SHOW_PLAN) {
  console.log("plan: 3 wings");
}
console.log("closed:", roomsClosed(12));

What does the program print? Is the plan line ever printed? Which values are known at compile time?

Reveal the reasoning

closed: 3. The plan line is never printed: SHOW_PLAN is a build-time setting that is false, so that if is dead code. In TypeScript, the compiler knows the names and the types, but no values: Node.js computes WINGS * ROOMS_PER_WING, and tests SHOW_PLAN, while the program runs. The values themselves are written in the code, so they are the same in every run. open is different: here the call passes 12, but roomsClosed must work for any caller, so its value is known only at run time.

The next lesson, What is a value?, looks at the numbers and text that these programs work with. A later lesson, Static and dynamic type checks, shows where to put the checks that only the run can make.

Practice tickets

  1. PHASE-101 · Late-opening evenings stop admitting visitors at 16:00 → Free
  2. PHASE-102 · The lift’s floor guide fails when a visitor types a floor that does not exist → Sign in · Pro
  3. PHASE-103 · The live booking app gives a room for 8 to a class of 30 → Sign in · Pro

Keep exploring

Observation and mental models →

Primary references