JavaScript Logging & Error Tracking: Console, Errors, and Monitoring
Logging and error tracking help you understand what your JavaScript code is doing, catch failures early, and diagnose problems after they happen. In the browser or on the server, good logging turns guesswork into a clear trail of events, warnings, and errors.
Quick answer: Use console methods for local debugging, use try...catch and throw for recoverable failures, and use an error-tracking service or centralized logs to capture issues you cannot reproduce locally.
Difficulty: Beginner
You'll understand this better if you know: basic JavaScript syntax, functions, objects, and how promises and async code work at a simple level.
1. What Is JavaScript Logging & Error Tracking?
JavaScript logging is the practice of writing useful messages during program execution so you can see what happened and in what order. Error tracking is the process of capturing failures, stack traces, and context so you can find, prioritize, and fix bugs.
- console.log() helps you inspect values while developing.
- console.warn() and console.error() separate warnings from failures.
- throw lets you stop execution when something is invalid.
- try...catch handles failures without crashing the whole flow.
- Error tracking systems record exceptions, stack traces, and request or user context.
2. Why JavaScript Logging & Error Tracking Matters
Without logs, many bugs are invisible until users report them. With logs, you can see the sequence of events that led to a problem, which values were passed into a function, and where the code failed.
Good error tracking also helps teams in production. If a bug only appears on one device, one browser, or one data set, recorded errors make it possible to diagnose it after the fact.
3. Basic Syntax or Core Idea
Logging and error tracking usually start with two ideas: write information out, and detect when something goes wrong. The simplest tools are console methods and the built-in Error object.
Basic logging
You can print values directly to the console to inspect state while the program runs.
const userName = "Ava";
const cartItems = 3;
console.log("User:", userName);
console.log("Items in cart:", cartItems);This example shows how simple messages can help you confirm that values exist and have the expected type.
Basic error creation
When a function cannot continue safely, it can stop by throwing an error.
function parseAge(value) {
if (typeof value !== "number") {
throw new Error("Age must be a number.");
}
return value;
}The caller can then decide whether to handle that failure or let it bubble up.
4. Step-by-Step Examples
Example 1: Tracing a function with console methods
Start with logs at the beginning, middle, and end of a function so you can see the control flow.
function formatTotal(subtotal, taxRate) {
console.log("formatTotal started", { subtotal, taxRate });
const tax = subtotal * taxRate;
console.debug("Calculated tax", tax);
const total = subtotal + tax;
console.log("formatTotal finished", total);
return total;
}This approach helps you locate whether the bug is in the input, the calculation, or the return value.
Example 2: Catching a recoverable error
Use try...catch when you can show a fallback or display a useful message instead of crashing.
function loadProfile(jsonText) {
try {
return JSON.parse(jsonText);
} catch (error) {
console.error("Invalid profile data:", error);
return null;
}
}This pattern is common when parsing user input or data returned from an API.
Example 3: Throwing a custom error for invalid input
When a caller gives you invalid arguments, fail early with a clear message.
function setDiscount(percent) {
if (percent < 0 || percent > 100) {
throw new RangeError("Discount must be between 0 and 100.");
}
return percent;
}Using a more specific error type makes the problem easier to classify and debug.
Example 4: Logging an async failure
Async operations often fail later, so you need to handle both promise rejections and thrown errors.
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Failed to fetch user", error);
throw error;
}
}This pattern logs the failure and still lets the caller decide whether to retry, show a message, or stop the flow.
5. Practical Use Cases
- Debugging a form submission that fails only in production because a required field is missing.
- Recording API failures, such as timeouts, 500 responses, or invalid JSON.
- Tracing event handlers so you can see which user action led to a bug.
- Capturing unexpected values before they break business logic, such as null or undefined.
- Reporting crashes from a browser app so the team can prioritize the most common failures.
- Auditing important app events like login attempts, retries, and validation failures.
6. Common Mistakes
Mistake 1: Logging too little context
Many beginners log only a short message, which makes production debugging much harder. A message like "Error happened" rarely tells you which user, value, or request caused the problem.
Problem: The log line does not include the values needed to reproduce the bug, so the error is hard to diagnose later.
console.error("Error happened");Fix: Include structured context that shows what the code was doing when the error occurred.
console.error("Error happened while saving profile", {
userId,
profileId,
attempt
});The corrected version works better because it gives you the exact context needed for investigation.
Mistake 2: Swallowing errors in catch blocks
Some code catches an error and does nothing with it. That makes failures disappear, which can hide broken logic and create silent data loss.
Problem: The error is caught, but the program continues as if nothing happened, so the failure becomes invisible.
try {
const data = JSON.parse(text);
return data;
} catch (error) {
// ignored
}Fix: Log the failure, return a safe fallback, or rethrow it so the caller can handle it.
try {
const data = JSON.parse(text);
return data;
} catch (error) {
console.error("Could not parse JSON", error);
return null;
}The corrected version makes the failure visible and gives the caller a predictable result.
Mistake 3: Throwing strings instead of Error objects
You can technically throw almost anything in JavaScript, but strings do not carry stack traces or standard error metadata. That makes production debugging much weaker.
Problem: Throwing a plain string removes the stack trace and makes the failure harder to inspect.
function validateName(name) {
if (!name) {
throw "Name is required";
}
}Fix: Throw an Error or one of its subclasses.
function validateName(name) {
if (!name) {
throw new Error("Name is required");
}
}The corrected version works better because standard error objects preserve stack traces and fit common tooling.
7. Best Practices
Practice 1: Log at the right level
Use different log levels to separate routine information from warnings and failures. This makes large logs easier to scan and filter.
console.info("Profile loaded");
console.warn("Using cached data");
console.error("Profile load failed");This pattern helps you separate expected warnings from genuine failures.
Practice 2: Use structured values instead of long strings
Objects are easier to search and more useful for log aggregation than a single concatenated sentence.
console.log("Checkout event", {
orderId,
total,
currency
});Structured data is easier to filter by field and easier to reuse in monitoring tools.
Practice 3: Rethrow when a lower layer cannot recover
If a function cannot meaningfully handle an error, it should log useful context and let the failure continue upward.
function readConfig(text) {
try {
return JSON.parse(text);
} catch (error) {
console.error("Invalid config format", error);
throw error;
}
}This keeps the failure visible while still adding local context.
8. Limitations and Edge Cases
- Logs in the browser console are mainly for developers; users do not normally see them.
- Messages can be lost or overwritten if the page crashes before telemetry is sent.
- Different browsers display objects and stack traces slightly differently.
- Minified production code can make stack traces harder to read unless source maps are enabled.
- console.error() does not automatically send data to your server or monitoring service.
- Unhandled promise rejections may not show up the same way as synchronous exceptions unless you track them explicitly.
Problem: A log message in the console does not mean the error has been recorded anywhere persistent.
For production tracking, you usually need both local logging and a system that stores errors remotely.
9. Practical Mini Project
Here is a small client-side feedback form that validates input, logs useful events, and reports errors to a centralized handler function.
const errorBuffer = [];
function trackError(error, context) {
const entry = {
message: error.message,
name: error.name,
stack: error.stack,
context
};
errorBuffer.push(entry);
console.error("Tracked error", entry);
}
function submitFeedback(name, message) {
try {
if (!name || !message) {
throw new Error("Both name and message are required.");
}
console.info("Feedback submitted", { name, message });
return { ok: true };
} catch (error) {
trackError(error, {
action: "submitFeedback",
name,
message
});
return { ok: false, error: error.message };
}
}
const result = submitFeedback("Mina", "Great product");
console.log("Result", result);This mini project shows a common pattern: validate input, log useful state, capture the error with context, and return a safe result to the caller.
10. Key Points
- Use logging to understand what your code is doing at runtime.
- Use errors to stop invalid work and make failures explicit.
- Use try...catch for recoverable problems such as parsing and network failures.
- Prefer Error objects over plain strings because they carry stack traces.
- Include enough context in logs to reproduce a bug later.
- For production, combine local logging with persistent error tracking.
11. Practice Exercise
- Write a function named createOrderSummary that accepts an array of item prices.
- Log the input array, reject non-arrays with a clear error, and return the total.
- If any price is not a finite number, throw a descriptive error.
Expected output: A number representing the total price, or a readable error message if the input is invalid.
Hint: Use Array.isArray(), Number.isFinite(), and throw new Error().
Solution:
function createOrderSummary(prices) {
console.log("Order prices", prices);
if (!Array.isArray(prices)) {
throw new TypeError("prices must be an array.");
}
let total = 0;
for (const price of prices) {
if (!Number.isFinite(price)) {
throw new RangeError("Every price must be a finite number.");
}
total += price;
}
console.info("Order total", total);
return total;
}
try {
const result = createOrderSummary([19.99, 5, 12.5]);
console.log("Final result", result);
} catch (error) {
console.error("Could not create order summary", error);
}12. Final Summary
JavaScript logging helps you see what is happening while your code runs, and error tracking helps you preserve failures in a way that is useful later. Together, they make debugging faster, reduce guesswork, and improve production reliability.
For everyday development, start with console.log(), then move to structured logging, console.error(), try...catch, and meaningful Error objects. For production systems, add persistent tracking so important failures are not lost when the page or process exits.
As a next step, practice adding logs to one small app, then replace one fragile silent failure with a real thrown error and a catch block that handles it cleanly.