Skip to content
academia.sh

Lesson 16 / 21

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.

Contents

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.

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:

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:

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:

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:

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:

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:

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.

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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close