---
title: 'Error Handling'
source: 'https://academia.sh/en/courses/javascript-fundamentals/error-handling'
course: 'JavaScript Fundamentals'
language: en
updated: '2026-08-17T18:09:46+00:00'
license: 'CC BY-SA 4.0'
---

# Error Handling

The flow of `try`/`catch`/`finally`, the `throw` statement, built-in error types, the error object's fields, the cause chain, and the choice between an error and a return value.

In earlier lessons, the `try`/`catch` structure was used to show error messages but was
never defined. This lesson completes the structure: the flow of the blocks, throwing
errors, built-in error types, and — most importantly — the decision of whether a
problem should be reported as an error or as a return value.

The Ways to Run Code lesson showed that an error object carries a name, a message, and
a stack trace. Here, we will produce these objects ourselves.

## The Flow of the Blocks

The `try` block contains the code being watched. If an error occurs, flow jumps to the
`catch` block. The `finally` block runs whether or not there was an error.

```js
function attempt(value) {
  try {
    console.log("1 try start");
    if (value < 0) throw new Error("negative value");
    console.log("2 try end");
    return "try return";
  } catch (error) {
    console.log("3 catch:", error.message);
    return "catch return";
  } finally {
    console.log("4 finally");
  }
}
console.log(attempt(1));
console.log("---");
console.log(attempt(-1));
```

```
1 try start
2 try end
4 finally
try return
---
1 try start
3 catch: negative value
4 finally
catch return
```

Two observations matter. First, when an error occurs, the `try` block's remaining
lines do not run; flow moves directly to `catch`. Second, the `finally` block runs
**after** the `return` statement but **before** the function actually returns. This is
why cleanup work is written there: closing an opened resource, releasing an acquired
lock.

Writing `return` inside `finally` overrides the return value:

```js
function overrides() {
  try {
    return "try";
  } finally {
    return "finally";
  }
}
console.log(overrides());
```

```
finally
```

The same behavior also swallows a thrown error. This is why the `finally` block should
contain only cleanup — it should carry no statement that redirects flow.

If the error object is not needed, the catch binding may be omitted:

```js
function isValidJson(text) {
  try {
    JSON.parse(text);
    return true;
  } catch {
    return false;
  }
}
console.log(isValidJson('{"a":1}'), isValidJson("{broken}"));
```

```
true false
```

## Built-in Error Types

The standard defines several error types, and errors the language itself produces are
among them:

```js
const attempts = [
  () => null.x,
  () => unknownName,
  () => ([].length = -1),
  () => (1234).toFixed(200),
];
for (const a of attempts) {
  try {
    a();
  } catch (error) {
    console.log(error.name + ": " + error.message);
  }
}
```

```
TypeError: Cannot read properties of null (reading 'x')
ReferenceError: unknownName is not defined
RangeError: Invalid array length
RangeError: toFixed() digits argument must be between 0 and 100
```

`TypeError` reports that a value was not of the expected kind, `ReferenceError` that a
name could not be found, `RangeError` that a value was outside the allowed range.
`SyntaxError` is produced during parsing; as seen in the first lesson, it never starts
the program at all — its one exception is the case where a built-in method that parses
text produces it at runtime.

Error **names** are standard; **messages** are not. Message text varies across
runtimes and across versions. This is why code should not branch on message text; it
should look at the name, the type, or a field we add ourselves.

## Throwing an Error

The `throw` statement can throw any value. What is thrown does not have to be an error
object, but it should be:

```js
try {
  throw "plain text";
} catch (error) {
  console.log(typeof error, error);
  console.log("name:", error.name, "| stack:", typeof error.stack);
}

try {
  throw new Error("error object");
} catch (error) {
  console.log(typeof error, error instanceof Error);
  console.log("name:", error.name, "| stack:", typeof error.stack);
}
```

```
string plain text
name: undefined | stack: undefined
object true
name: Error | stack: string
```

When text is thrown, there is no name and no stack trace; where the error came from is
lost. This course's rule: **every thrown value is an error object.**

When wrapping a lower-layer error, the original error should be preserved. An error
object can carry a **cause** field for this:

```js
function parseTemperature(text) {
  const value = Number(text);
  if (Number.isNaN(value)) {
    throw new Error(`could not convert to number: ${JSON.stringify(text)}`);
  }
  return value;
}

function readRecord(line) {
  const [station, temperatureText] = line.split(";");
  try {
    return { station, temperature: parseTemperature(temperatureText) };
  } catch (error) {
    throw new Error(`record could not be read: ${station}`, { cause: error });
  }
}

console.log(readRecord("A1;21.5"));
try {
  readRecord("B2;abc");
} catch (error) {
  console.log(error.message);
  console.log("cause:", error.cause.message);
}
```

```
{ station: 'A1', temperature: 21.5 }
record could not be read: B2
cause: could not convert to number: "abc"
```

The upper layer added its own context (which record), the lower layer's information
(the cause) was not counted as lost. This is the standard way to enrich error messages
with context.

## Marking Your Own Error Type

It should be possible to tell apart an error we produced ourselves; otherwise a
`catch` block swallows unexpected programming errors along with the code's own errors.

The simplest way is to add a name and a code field to the error object:

```js
function measurementError(code, message) {
  const error = new Error(message);
  error.name = "MeasurementError";
  error.code = code;
  return error;
}

const e = measurementError("EMPTY_FIELD", "temperature field is empty");
console.log(e.name, "|", e.code, "|", e.message, "|", e instanceof Error);
```

```
MeasurementError | EMPTY_FIELD | temperature field is empty | true
```

Defining named error classes through inheritance — using class syntax — is the next
course's subject. In this course, a name and a code field are enough.

## Applying It to the Measurement Script

The response to broken data can now be tied to a contract: every line is either
accepted or rejected with a reason; no line silently turns into a wrong value.

```js
const CODE_EMPTY = "EMPTY_FIELD";
const CODE_NOT_NUMBER = "NOT_A_NUMBER";

function measurementError(code, message) {
  const error = new Error(message);
  error.name = "MeasurementError";
  error.code = code;
  return error;
}

function readTemperature(text) {
  if (text.trim() === "") {
    throw measurementError(CODE_EMPTY, "temperature field is empty");
  }
  const value = Number(text);
  if (Number.isNaN(value)) {
    throw measurementError(CODE_NOT_NUMBER, `temperature could not be converted to a number: ${JSON.stringify(text)}`);
  }
  return value;
}

const lines = ["A1;21.5", "A2;", "B1;abc", "B2;0"];
const accepted = [];
const rejected = [];

for (const line of lines) {
  const [station, field] = line.split(";");
  try {
    accepted.push({ station, temperature: readTemperature(field) });
  } catch (error) {
    if (error.name !== "MeasurementError") throw error;
    rejected.push({ station, code: error.code, message: error.message });
  }
}

console.log("accepted:", accepted);
console.log("rejected:", rejected);
```

```
accepted: [
  { station: 'A1', temperature: 21.5 },
  { station: 'B2', temperature: 0 }
]
rejected: [
  {
    station: 'A2',
    code: 'EMPTY_FIELD',
    message: 'temperature field is empty'
  },
  {
    station: 'B1',
    code: 'NOT_A_NUMBER',
    message: 'temperature could not be converted to a number: "abc"'
  }
]
```

Three decisions separate this script from its earlier versions.

First, the `0` value in the `B2` record was accepted. The distinction built in the
Truthiness lesson holds: zero is a valid measurement.

Second, the catch block **recognizes its own error**. The check
`error.name !== "MeasurementError"` rethrows an unexpected error. Without this line, a
`TypeError` arising, say, in the `line.split` call would also be counted as a
"rejected record," and a programming error would look like a data error.

Third, the rejected list carries a reason. How many records were rejected and why can
be read straight from the output.

## Error or Return Value

Not every unusual situation is an error. The criterion is this: **if the caller can
anticipate the situation, a return value fits; if not, an error fits.**

Asking for the average of an empty list is a foreseeable situation; the `average`
function in the Function Declaration and Expression lesson returned `null` for exactly
this reason. In contrast, finding a letter in a field that should be numeric is a
violation of the data's contract; an error fits.

Both practices sit together in the script above: `readTemperature` throws an error, the
calling loop catches the error and turns it into a data structure — the rejected list.
The boundary is drawn by where the problem can be handled.

Errors are not used for flow control. Throwing an error to exit a loop, skip a value,
or signal a condition is both costly and hard to read; `break`, `continue`, and return
values are what this job is for.

## Summary

- If an error occurs in the `try` block, the remaining lines do not run; `finally` runs
  in every case and after `return` is evaluated.
- Writing `return` or `throw` inside `finally` overrides the return value and the
  error; the block should contain only cleanup.
- Error names are standard, messages are not; branching is not done on message text.
- Every thrown value should be an error object; otherwise there is no name and no
  stack trace.
- Wrapped errors are preserved with the `cause` field; your own errors are marked with
  a name and a code field, and unrecognized errors are rethrown.
- A foreseeable situation is reported with a return value, a contract violation with an
  error.

## Next Step

Control flow and functions are complete. In this topic's examples, structures like
`filter`, `map`, `split`, and object spread were used unexplained. The next topic
defines these: arrays' construction and transformation methods, object access
shortcuts, template literals, JSON conversions, and date–time representation. There,
the measurement list will turn into a complete data set that is read from text and
written back to text.
