All the code you have written in this module assumes the data is correct: that estimatedHours is a number, that priority is 'high', 'medium' or 'low', that the due date is in ISO format. As soon as Nómada Tasks has a form, that assumption will break on day one: Iván will type "two" where the hours go, someone will leave the title blank and someone else will enter a date from last year. With no protection, the program does one of two things, both bad: it produces a silent NaN that contaminates every later calculation, or it stops dead with a message Marta cannot understand. In this lesson you will learn to detect those situations, to interrupt execution with an error of your own and a useful message, and to catch it so you can decide what to do without the whole application collapsing.
Contents
- Syntax errors and exceptions: not the same thing
- What happens when nobody catches an exception
tryandcatchfinallyand the full flow- The
Errorobject:name,messageandstack - The built-in error types
throw: throwing your own errors- Why you throw an
Errorand not a string - Custom errors: the short recipe
- Validating input and failing fast
- What NOT to catch: silencing errors is an antipattern
- Asynchronous errors are another story
- Case study: accepting new tasks from the form
- Common Mistakes and Tips
- Exercises
- Conclusion
- Syntax errors and exceptions: not the same thing
When you learned to read a console error in lesson 01-03, we did not distinguish between two very different things. Now we need to.
| Syntax error | Exception (runtime error) | |
|---|---|---|
| When it appears | While reading the code, before anything runs | During execution, on a specific line |
| Cause | The code is not valid JavaScript | The data or the state is not what was expected |
| Example | const x = ; |
null.title |
| How much code runs | None of the affected file | Everything up to the failing line |
| Can it be caught? | No, not with a normal try/catch |
Yes |
| How it is fixed | By correcting the code | By validating data or by catching |
A syntax error is a programmer's mistake, it is detected before running and there is no sense in "handling" it: it gets fixed. An exception is an anomalous situation that happens while the program is running, almost always because the data was not what you expected. That is the one you handle.
flowchart TD
A["The engine reads the file"] --> B{"Valid syntax?"}
B -->|no| C["SyntaxError<br/>NOTHING runs"]
B -->|yes| D["Execution starts"]
D --> E{"Anomalous situation<br/>on some line?"}
E -->|no| F["The program ends cleanly"]
E -->|yes| G["An exception is thrown"]
G --> H{"Is there a try/catch<br/>around it?"}
H -->|yes| I["catch decides what to do<br/>the program continues"]
H -->|no| J["The program stops"]
- What happens when nobody catches an exception
When an exception is thrown and nobody picks it up, execution stops right there. Nothing that came afterwards runs.
const assignee = null;
console.log('Before');
console.log(assignee.length); // ✗ TypeError
console.log('After'); // ← never runsOutput:
The key word is Uncaught: it means "nobody has caught this". In Node.js the process exits with an error code; in the browser the current block of code stops, although the rest of the page stays alive and the buttons that already had listeners will keep responding.
That behavior —stopping— is deliberate and correct. If the program carried on with broken data, it would produce false results that nobody would notice. Stopping at the exact point of failure is preferable.
What try/catch gives you is not avoiding the stop: it is deciding what stops. One bad task in the form should not prevent the other four from being processed.
try and catch
try and catchThe basic structure has two blocks:
With an example from the project: a task's data arrives as JSON text from browser storage and it may be corrupted.
const savedText = '{ this is not valid JSON }';
try {
const data = JSON.parse(savedText);
console.log('Tasks recovered:', data);
} catch (error) {
console.error('The saved tasks could not be read.');
console.error('Technical reason:', error.message);
}
console.log('The application continues.');Output:
The saved tasks could not be read. Technical reason: Expected property name or '}' in JSON at position 2 The application continues.
The essentials:
- The
tryblock runs normally until something fails. If nothing fails,catchis skipped entirely. - When something fails, execution jumps to the
catchimmediately. The rest of thetrydoes not run: in the example, theconsole.log('Tasks recovered...')never gets to run. erroris a parameter that receives the object describing the failure. You can call it whatever you like;error,errandeare the usual names.- The program continues after the
catch. That is the whole magic.
If you do not need the error object, modern JavaScript lets you omit the parentheses:
try {
JSON.parse(savedText);
} catch {
console.warn('Corrupted data: starting with an empty board.');
}Keep the try as small as possible. Wrapping fifty lines in a single try means any of them could have failed and the catch has no idea which. Wrap only the operation that can genuinely fail.
finally and the full flow
finally and the full flowfinally is an optional third block that always runs: whether something failed or not, whether it was caught or not.
let processedTasks = 0;
try {
console.log('1. Opening the board...');
throw new Error('Storage is not responding');
console.log('2. This line never runs');
} catch (error) {
console.error(`3. Caught: ${error.message}`);
} finally {
console.log('4. Closing the board (no matter what)');
}
console.log('5. The program carries on');Output:
1. Opening the board... 3. Caught: Storage is not responding 4. Closing the board (no matter what) 5. The program carries on
flowchart TD
A["Enters the try"] --> B{"Does something fail?"}
B -->|no| C["The whole try finishes"]
B -->|yes| D["Jumps to the catch<br/>The rest of the try is discarded"]
C --> E["finally"]
D --> E
E --> F["Continues after the block"]
finally is there for the cleanup work that has to happen no matter what: closing a connection, hiding a loading indicator, unlocking a button. In Nómada Tasks its typical case will be this one, already in Module 6:
// A sketch of what you will do with the DOM
try {
// save the task
} catch (error) {
// show the error message to the user
} finally {
// re-enable the "Save" button, whether it failed or not
}Without finally you would have to repeat that line in the try and in the catch. With it you write it once and it runs for certain.
An important detail: finally runs even if the catch rethrows the error. And it also runs if there is no catch at all: try/finally with no catch is a legal and useful combination when you want to clean up but let the error travel upward.
- The
Error object: name, message and stack
Error object: name, message and stackWhat the catch receives is an object with three fundamental properties:
| Property | What it holds | Example |
|---|---|---|
name |
The type of error | 'TypeError' |
message |
The human-readable description | 'Cannot read properties of null' |
stack |
The call stack: where it happened and how you got there | Multi-line text with files and line numbers |
try {
const task = null;
console.log(task.title);
} catch (error) {
console.log('Type: ', error.name); // TypeError
console.log('Message: ', error.message); // Cannot read properties of null (reading 'title')
console.log('Stack:\n', error.stack);
}name and message are the ones you will use every day: name to decide what to do and message to explain what happened.
stack is the diagnostic tool. It contains the route execution took up to the point of failure, with file names and line numbers. It reads top to bottom: the first line is where the error occurred and the following ones are what led to it. In Module 3, when you have functions calling functions, the stack will become genuinely informative; today it has one or two lines.
Log the complete error, not just the message. console.error(error) in the browser console shows the whole object with its collapsible stack; console.error(error.message) throws that information away and leaves you with no idea where it happened.
- The built-in error types
JavaScript defines several error types. They all share name, message and stack, and they differ in the kind of problem they represent.
| Type | When it is thrown | Typical example in the project |
|---|---|---|
TypeError |
A value is not of the expected type, or a property of null/undefined is accessed |
task.title when task is null |
ReferenceError |
A variable that does not exist, or is not initialized yet, is used | Writing priorty instead of priority |
RangeError |
A numeric value is outside the allowed range | new Array(-1); you will also use it yourself for hours outside 0–40 |
SyntaxError |
The code is not valid JavaScript | Detected before running; also thrown by JSON.parse with corrupted text |
URIError |
Incorrect use of the URL encoding functions | decodeURIComponent('%') |
EvalError |
Practically obsolete | — |
The first three are the ones you will see 95% of the time. Check them out:
// TypeError
try { null.title; } catch (e) { console.log(e.name); } // TypeError
// ReferenceError
try { console.log(priorty); } catch (e) { console.log(e.name); } // ReferenceError
// RangeError
try { new Array(-1); } catch (e) { console.log(e.name); } // RangeError
// SyntaxError at runtime, via JSON.parse
try { JSON.parse('{'); } catch (e) { console.log(e.name); } // SyntaxErrorKnowing the type lets you react differently to each one:
try {
JSON.parse(savedText);
} catch (error) {
if (error.name === 'SyntaxError') {
console.warn('The saved data is corrupted. We start from scratch.');
} else {
console.error('Unexpected failure while reading the board:', error);
}
}There is a more idiomatic way of making that check —error instanceof SyntaxError—, but instanceof belongs to the world of prototypes and classes, which is studied in Module 5. Comparing error.name with a string works perfectly well and is what we will use for now.
throw: throwing your own errors
throw: throwing your own errorsSo far you have caught errors thrown by JavaScript. The other half of the mechanism is throwing them yourself, with throw, when you detect that a business rule is not being met.
const estimatedHours = 52;
try {
if (estimatedHours > 40) {
throw new RangeError(`R3 violated: ${estimatedHours} h exceeds the maximum of 40.`);
}
console.log('Task accepted.');
} catch (error) {
console.error(`[${error.name}] ${error.message}`);
}
// [RangeError] R3 violated: 52 h exceeds the maximum of 40.What throw does, exactly:
- It stops execution at that point. Like the
breakfrom the previous lesson, but far more radical: it does not jump to the next iteration, it abandons the wholetryblock. - It looks for the nearest
catchwrapping that line. - If there is none, the error is left
Uncaughtand the program stops.
new Error(...) creates the error object. The text you pass it becomes its message. You can use the generic Error or whichever type best describes the problem: RangeError for out-of-range values, TypeError for incorrect types.
How to write a good error message. The message will be read by someone trying to fix something, so it has to contain three things:
| Ingredient | Bad | Good |
|---|---|---|
| What failed | 'Error' |
'estimatedHours out of range' |
| What value caused it | 'Invalid value' |
'got 52' |
| What was expected | — | 'must be between 0 and 40' |
All together:
throw new RangeError(`estimatedHours out of range: got ${estimatedHours}, must be between 0 and 40.`);That message explains itself, without opening the code.
- Why you throw an
Error and not a string
Error and not a stringthrow accepts any value. This is legal:
And it is a bad idea for three concrete reasons:
1. You lose the stack. A string has no stack, so inside the catch there is no way to know on which line the problem started. In projects with more than one file, this turns debugging into guesswork.
2. You lose the type. error.name is undefined, so you cannot react differently depending on the kind of problem.
3. You break the code that catches. A well-written catch reads error.message. If a string arrives, error.message is undefined and the message is lost. Worse: if someone throws null, error.message itself causes a TypeError inside the catch.
Compare them:
try {
throw 'something is wrong';
} catch (error) {
console.log(error.name); // undefined
console.log(error.message); // undefined
console.log(error); // something is wrong
}
try {
throw new Error('something is wrong');
} catch (error) {
console.log(error.name); // Error
console.log(error.message); // something is wrong
console.log(error.stack); // Error: something is wrong\n at ...
}A rule with no exceptions: always throw an Error object or one of its derived types.
- Custom errors: the short recipe
When you want to tell your business errors apart from the language's own, you can create a type of your own. The recipe is this one, and for now it is enough to copy it:
class ValidationError extends Error {
constructor(message, field) {
super(message); // passes the message to the base Error
this.name = 'ValidationError';
this.field = field; // extra data: which field failed
}
}It is used just like any other error, with the advantage of carrying additional information:
const title = ' ';
try {
if (title.trim() === '') {
throw new ValidationError('The title cannot be empty (R2).', 'title');
}
} catch (error) {
if (error.name === 'ValidationError') {
console.error(`Field "${error.field}": ${error.message}`);
} else {
console.error('Unexpected error:', error);
}
}
// Field "title": The title cannot be empty (R2).The practical advantage is twofold: you can filter in the catch the errors you know how to deal with and let the rest through, and you can attach data —the field, the value received, the task id— that the form will use to highlight the right box in red.
Exactly what class, extends, constructor, super and this mean is explained in Classes and Object-Oriented Programming. Here it is just a template you can reuse by changing the name.
- Validating input and failing fast
The principle is called fail fast: checking the data at the moment it enters the system and rejecting it right there, instead of letting it move on and discovering the problem three layers deeper.
The difference is clearest with an example. Without validation:
const hoursText = 'twelve'; // arrives from the form
const hours = Number(hoursText); // NaN
const backlogTotal = 45 + hours; // NaN
const average = backlogTotal / 6; // NaN
console.log(`Average per task: ${average} h`); // Average per task: NaN hThe NaN causes no error at all: it spreads silently through every calculation and shows up at the end, in a report, with no clue as to which task was to blame. This is exactly the scenario lesson 01-07 warned you about.
With validation at the entry point:
const hoursText = 'twelve';
try {
const hours = Number(hoursText);
if (Number.isNaN(hours)) {
throw new TypeError(`estimatedHours must be a number; got "${hoursText}".`);
}
if (hours <= 0 || hours > 40) {
throw new RangeError(`estimatedHours out of range: ${hours}. Must be between 0 and 40.`);
}
console.log(`Hours accepted: ${hours}`);
} catch (error) {
console.error(`Task rejected — [${error.name}] ${error.message}`);
}
// Task rejected — [TypeError] estimatedHours must be a number; got "twelve".The failure is detected at the exact point where the bad value came in, the message names the field and the value received, and no later calculation gets contaminated.
Notice that it uses Number.isNaN(hours) and not isNaN(hours): the distinction from lesson 01-07 still holds, because isNaN converts before checking and gives false positives.
The order of the validations matters. Type first, range afterwards. Checking hours > 40 when hours is NaN gives false, and the validation would pass without detecting anything: every comparison with NaN is false.
- What NOT to catch: silencing errors is an antipattern
try/catch is a powerful tool, and like every powerful tool it is easy to misuse. These are the three abuses to avoid.
1. The empty catch. The worst of them all.
This does not handle the error: it hides it. The program carries on with an incorrect state, without leaving a trace in the console, and the failure will surface later somewhere completely different. Debugging that can cost hours. If a specific error really is expected and ignorable, write it out explicitly:
} catch (error) {
// It is normal to have no saved data the first time: we start empty.
console.info('No previous board; a new one is created.');
}2. Catching programming errors. A TypeError caused by misspelling a variable name is a bug, not an exceptional situation. Wrapping it in a try/catch does not fix it: it hides it. Bugs get fixed; you only catch the situations the program does not control.
| Situation | try/catch? | Why |
|---|---|---|
| Data typed in by a user | ✓ Yes | You do not control it |
| A file or an API that may fail | ✓ Yes | It depends on the outside world |
JSON.parse of saved data |
✓ Yes | It may be corrupted |
| A misspelled variable name | ✗ No | It is a bug: fix it |
| An array you know exists | ✗ No | If it fails, your logic is wrong |
3. The giant try. Wrapping a hundred-line block means the catch cannot know what failed nor react sensibly. Wrap the risky operation, not the whole program.
And one pattern that is correct: catch, enrich and rethrow.
try {
JSON.parse(savedText);
} catch (error) {
console.error('Failed to read the saved board:', error.message);
throw error; // I am not silencing it: I add context and let it travel up
}This is what you will do in intermediate layers: logging useful information without deciding yourself what to do about the problem.
- Asynchronous errors are another story
An important warning so that it does not take you by surprise later on: try/catch only catches errors from code that runs right now, inside the try block. If inside the try you start an operation that will finish later —a request to a server, a timer—, the try will have finished long before that operation fails, and the catch will never know.
// ✗ The catch does NOT catch any of this
try {
setTimeout(function () {
throw new Error('Late failure');
}, 1000);
} catch (error) {
console.error('This never runs');
}The error is thrown a second later, when the try/catch no longer exists, and it ends up Uncaught.
Error handling in asynchronous code has its own tools: .catch() on promises and try/catch combined with await, which does work. All of that is studied in Promises and Async/Await. For now, hold on to the rule: try/catch protects synchronous code.
- Case study: accepting new tasks from the form
We close the module by putting everything together. Marta has collected five task requests from a form and they need processing: accept the valid ones, reject the ones that break the rules and —this is the important part— make sure a bad request does not prevent the following ones from being processed.
The data arrives as text, exactly as it comes out of an HTML form.
const TODAY = '2026-09-20';
const TEAM = ['Marta', 'Iván', 'Lucía'];
const reqTitles = [
'Replace the multipurpose room lamp',
' ',
'Buy white screen-printing ink',
'Replace the lathe blade',
'File the quarterly invoices'
];
const reqAssignees = ['Lucía', 'Marta', 'Iván', 'Iván', 'Marta'];
const reqPriorities = ['medium', 'high', 'medium', 'urgent', 'low'];
const reqHours = ['4', '3', 'two', '10', '3'];
const reqDates = ['2026-10-20', '2026-10-01', '2026-10-01', '2026-10-05', '2026-08-01'];
let accepted = 0;
let rejected = 0;
let acceptedHours = 0;
for (let i = 0; i < reqTitles.length; i++) {
try {
// R2 — non-empty title of a reasonable length
const title = reqTitles[i].trim();
if (title === '') {
throw new Error('R2: the title cannot be empty.');
}
if (title.length > 100) {
throw new RangeError(`R2: the title has ${title.length} characters; the maximum is 100.`);
}
// R8 — assignee is null or on the team
const assignee = reqAssignees[i];
let isOnTeam = false;
for (let p = 0; p < TEAM.length; p++) {
if (TEAM[p] === assignee) {
isOnTeam = true;
break;
}
}
if (assignee !== null && !isOnTeam) {
throw new Error(`R8: "${assignee}" does not belong to the Taller Nómada team.`);
}
// Priority within the closed set
const priority = reqPriorities[i];
if (priority !== 'high' && priority !== 'medium' && priority !== 'low') {
throw new Error(`Invalid priority: "${priority}". Use high, medium or low.`);
}
// R3 — hours: type first, range afterwards
const hours = Number(reqHours[i]);
if (Number.isNaN(hours)) {
throw new TypeError(`R3: estimatedHours must be a number; got "${reqHours[i]}".`);
}
if (hours <= 0 || hours > 40) {
throw new RangeError(`R3: ${hours} h out of range. Must be between 0 and 40.`);
}
// R4 — the due date cannot be in the past
const date = reqDates[i];
if (date < TODAY) {
throw new RangeError(`R4: the due date ${date} is earlier than today (${TODAY}).`);
}
// If we have got this far, the request is valid
accepted++;
acceptedHours += hours;
console.log(`✓ Accepted: ${title} · ${assignee} · ${priority} · ${hours} h · ${date}`);
} catch (error) {
rejected++;
console.error(`✗ Request ${i + 1} rejected — [${error.name}] ${error.message}`);
}
}
console.log('---');
console.log(`Accepted: ${accepted}`);
console.log(`Rejected: ${rejected}`);
console.log(`Hours added to the backlog: ${acceptedHours} h`);Output:
✓ Accepted: Replace the multipurpose room lamp · Lucía · medium · 4 h · 2026-10-20 ✗ Request 2 rejected — [Error] R2: the title cannot be empty. ✗ Request 3 rejected — [TypeError] R3: estimatedHours must be a number; got "two". ✗ Request 4 rejected — [Error] Invalid priority: "urgent". Use high, medium or low. ✗ Request 5 rejected — [RangeError] R4: the due date 2026-08-01 is earlier than today (2026-09-20). --- Accepted: 1 Rejected: 4 Hours added to the backlog: 4 h
Go over the design decisions, because these are the ones you will apply throughout the project:
- The
tryis inside the loop, not outside. This is the key to the whole example. With thetrywrapping the entire loop, the first invalid request would have aborted the process and the following three would never have been checked. By putting it inside, each request is processed independently and one bad value only affects its own row. throwworks as a perfect early exit. It is the guard clause from lesson 02-01, this time with a real exit: as soon as one rule fails, the remaining validations for that request are discarded. There is no nesting, noerrorvariable to carry around, noelse.- Each
throwpicks its error type.TypeErrorfor a wrong type,RangeErrorfor out-of-range values, genericErrorfor the other business rules. With that, thecatchcould react differently to each family. - The order of the validations is deliberate. Type before range, always. And the cheap checks before the expensive ones, as you learned in lesson 02-04.
- All four structures from the module appear. The outer
forthat walks the requests, the innerforwith abreakthat looks for the assignee on the team, theifstatements of each validation and thetry/catchwrapping everything. That is the whole module working together.
And now notice something uncomfortable: the validation block is forty lines long and only works for this. If tomorrow you have to validate an edited task instead of a new one, you have to copy the whole thing.
Common Mistakes and Tips
The empty catch. You have seen it already, but it bears repeating: it is the worst mistake in this lesson. A catch that logs nothing turns a localized failure into a mystery.
Throwing strings instead of Error objects. You lose name and stack, and you break any catch that expects a real error.
Putting the try outside the loop when it belonged inside. One bad input aborts the entire process. Always ask yourself: should a failure here stop everything, or only this iteration?
Using try/catch for what an if solves. Checking whether a value is negative does not need exceptions: it needs a condition. Exceptions are for the exceptional; if the "error" happens in the normal flow, it is a case, not an exception.
Expecting try/catch to catch asynchronous errors. It does not. setTimeout, fetch and promises need different treatment, which you will see in Module 5.
Checking the range before the type. With NaN every comparison gives false, so a non-numeric value passes any range validation undetected.
Logging only error.message. You lose the stack. Use console.error(error) while you are debugging.
Tip: write the message thinking of whoever is going to read it at three in the morning. What failed, what value caused it and what was expected. All three ingredients, every time.
Tip: include the rule identifier in the message. An R3: at the start of the text connects the error with the project specification and saves a lot of time.
Tip: test your validations with bad data on purpose. Deliberately write 'two', -5, '' and null and check that each one produces the message you expected. It is the seed of the unit tests in Module 8.
Exercises
Exercise 1 — Robust reading of the saved board
Write a try/catch/finally block that tries to read the contents of the variable saved with JSON.parse. If the text is valid, show a success message; if it is corrupted, warn that an empty board is being started. In finally, always print "Board ready". Test it with '[1,2,3]' and with '{ broken', and explain why using if instead of try/catch would not be feasible here.
Exercise 2 — A transition validator that throws errors
Rewrite the status-change validation from lesson 02-01 using throw instead of the allowed and reason variables. It must throw an Error with a descriptive message when the new status does not exist, when it is the same as the current one, or when the transition is not permitted by R6. Wrap it in a try/catch and test it with pending → done and with in-progress → done.
Exercise 3 — Custom errors with the offending field
Using the recipe from section 9, create ValidationError with the properties field and receivedValue. Validate a task with title = '', hours = '35' and priority = 'low', and make the catch distinguish between your validation errors —showing the offending field— and any other unexpected error.
Solutions
Exercise 1
const saved = '{ broken';
try {
const board = JSON.parse(saved);
console.log(`Board recovered with ${board.length} item(s).`);
} catch (error) {
console.warn(`Saved data is unreadable (${error.name}). Starting with an empty board.`);
} finally {
console.log('Board ready.');
}With '[1,2,3]':
With '{ broken':
Why an if is no use: to know whether a string is valid JSON you would have to analyze it character by character, that is, reimplement the whole of JSON.parse. There is no reasonable check you can make beforehand. This is the archetypal case for try/catch: the only way to know whether the operation works is to attempt it. When you can check it beforehand with a condition, use the condition; when you cannot, catch.
The finally guarantees that the application continues down both paths with the same message, without duplicating it in the two blocks.
Exercise 2
const currentStatus = 'pending';
const newStatus = 'done';
try {
const isValidStatus =
newStatus === 'pending' || newStatus === 'in-progress' || newStatus === 'done';
if (!isValidStatus) {
throw new TypeError(`"${newStatus}" is not a status of the system.`);
}
if (currentStatus === newStatus) {
throw new Error(`The task is already in status "${currentStatus}".`);
}
const isValidTransition =
(currentStatus === 'pending' && newStatus === 'in-progress') ||
(currentStatus === 'in-progress' && newStatus === 'done') ||
(currentStatus === 'in-progress' && newStatus === 'pending') ||
(currentStatus === 'done' && newStatus === 'in-progress');
if (!isValidTransition) {
throw new Error(`R6: transition not allowed ${currentStatus} → ${newStatus}.`);
}
console.log(`✓ Status updated to "${newStatus}".`);
} catch (error) {
console.error(`✗ [${error.name}] ${error.message}`);
}With pending → done:
With in-progress → done:
Compare this version with the one from lesson 02-01: there you needed the allowed and reason variables, and every branch had to assign both. Here each guard either throws or lets things through, and the happy path —the final console.log— sits at the same level of indentation as everything else.
Notice too that the four valid transitions have been grouped into a single boolean expression with parentheses, applying the rule from lesson 02-01: when && and || are mixed, the parentheses go in even though precedence is already correct.
Exercise 3
class ValidationError extends Error {
constructor(message, field, receivedValue) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.receivedValue = receivedValue;
}
}
const title = '';
const hoursText = '35';
const priority = 'low';
try {
if (title.trim() === '') {
throw new ValidationError('R2: the title is required.', 'title', title);
}
const hours = Number(hoursText);
if (Number.isNaN(hours)) {
throw new ValidationError('R3: the hours must be a number.', 'estimatedHours', hoursText);
}
if (hours <= 0 || hours > 40) {
throw new ValidationError('R3: the hours must be between 0 and 40.', 'estimatedHours', hours);
}
if (priority !== 'high' && priority !== 'medium' && priority !== 'low') {
throw new ValidationError('Unrecognized priority.', 'priority', priority);
}
console.log('✓ Valid task.');
} catch (error) {
if (error.name === 'ValidationError') {
console.error(`✗ Field "${error.field}" — ${error.message} (received: ${JSON.stringify(error.receivedValue)})`);
} else {
console.error('✗ Unexpected error, this is a bug:', error);
}
}
// ✗ Field "title" — R2: the title is required. (received: "")Three things that make this pattern valuable:
error.fieldlets the interface react. In Module 6, that piece of data will highlight in red exactly the form box that needs correcting, instead of showing a generic warning.JSON.stringify(error.receivedValue)shows the value in quotes, which tells the empty string""apart from a space" "or fromnull. A plainconsole.logwould print all three indistinguishably.- The
elsebranch silences nothing. Errors that are not validation errors —aTypeErrorfrom a bug of yours— are logged in full, with their stack. Filtering what you know how to handle and leaving the rest visible is the difference between handling errors and hiding them.
Change title to 'Refurbish the lathe' and you will see the validation run all the way through and print ✓ Valid task., because 35 h is within range and 'low' is a correct priority.
Conclusion
With this lesson you close Module 2. You can now tell a syntax error —a fault in the code, which gets fixed— from an exception —an anomalous situation at runtime, which gets handled. You know that an uncaught exception stops the program, and that try/catch does not prevent the stop but lets you decide what stops: in the form example, one invalid request no longer prevents the other four from being processed. You know finally for the cleanup that has to happen no matter what, the Error object with its name, its message and its stack, and the built-in types that let you react differently to each kind of problem.
You know how to throw your own errors with throw, always with an Error object and never with a string, and to write messages that say what failed, what value caused it and what was expected. You have the recipe for custom errors so you can attach the offending field. And —just as important— you know what not to catch: an empty catch does not handle an error, it hides it; bugs get fixed rather than wrapped; and asynchronous errors need the tools from Module 5.
Look back and check what you have gained across these five lessons. Your code decides with if/else if and with switch, repeats with for, while and do...while, walks the entire backlog accumulating, counting, finding extremes and crossing lists with nested loops, cuts out at exactly the right moment with break and continue, and defends itself against bad data with validations that fail fast and with useful messages. That is a real program already.
And you have also been building up a complaint, lesson after lesson. You have written the chain that turns priority into a weight four times. The transition validation appears in two lessons almost identically. The forty-line block that validates a new task cannot be reused to validate an edited task without copying the whole thing. And when you wanted to leave two loops at once, the cleanest solution turned out to be one you could not write yet. Everything points to the same place: you need to be able to give a name to a piece of logic, store it and use it as many times as you like, with different data each time. That is a function, and it is exactly what starts in Module 3: Functions, with Defining and Calling Functions. From there on, validateTask(), calculateUrgency() and canChangeStatus() will stop being copied blocks and become named pieces you can combine, test and reuse throughout Nómada Tasks.
JavaScript Course: From Beginner to Advanced
Module 1: Introduction to JavaScript
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
