Debugging Async JavaScript
Asynchronous Bugs
Asynchronous code can be more difficult to debug than synchronous code.
The code often executes later than expected, and several asynchronous operations may run at the same time.
This chapter shows techniques for finding and fixing common problems in asynchronous JavaScript.
This chapter shows practical ways to debug fetch(), promises, and async/await.
Async debugging is about finding where the code stops.
Then to find why it stopped.
Log Events
The easiest way to debug asynchronous code is to log when things happen.
Use console.log()
console.log("Before fetch");
const promise = fetch("fetch.txt");
console.log("After fetch");
Try it Yourself »
Use a display function
myDisplayer("Before fetch");
const promise = fetch("fetch.txt");
myDisplayer("After fetch");
Try it Yourself »
Note
console.log(), often display the current state of a Promise like:
- <pending>
- <fulfilled>
- <rejected>
This information is provided by the debugging tool. It is not part of the JavaScript language.
Why can't JavaScript do the same?
There is no standard properties like:
- promise.state
- promise.status
They simply do not exist.
The state part of the Promise is not in its public API.
Async Bugs Can be Invisible
Async code often fails after your function has already returned.
This makes it feel like nothing happened.
Example
// Async function to download a file
async function loadText(file) {
const response = await fetch(file);
myDisplayer(response);
}
myDisplayer("Start");
// Try to load a missing file
loadText("missing.txt");
myDisplayer("Done");
Try it Yourself »
The Done message prints even if loadText() fails.
This is because the fetch() fails later.
Rule 1: Always Handle Errors
Unhandled promise rejections confuse beginners.
Handle errors early and clearly.
Use try...catch to handle Promises.
Example
// Async function to download a file
async function loadData(file) {
try {
let response = await fetch(file);
let data = await response.json()
myDisplayer(JSON.stringify(data));
} catch (error) {
myDisplayer(error);
}
}
// Call async function
loadData("customer.json")
Try it Yourself »
Note
The code above catches network errors and JSON parsing errors.
It does not catch HTTP errors like 404 (file not found).
Rule 2: Check response.ok
Fetch does not reject on HTTP errors like 404.
You must check response.ok.
Example
// Async function to download a file
async function loadData(file) {
try {
let response = await fetch(file);
if (!response.ok) {
console.log("HTTP Error:", response.status);
return;
}
let data = await response.json()
myDisplayer(JSON.stringify(data));
} catch (error) {
myDisplayer(error);
}
}
Try it Yourself »
The code above separates HTTP errors from network errors.
Most fetch bugs are not JavaScript bugs.
They are URL and server response bugs.
Rule 3: Log the Right Things
Logging a promise is not the same as logging the data.
Log the response and status first.
Example
// Async function to download a file
async function loadData(file) {
let response = await fetch(file);
myDisplayer(response.status);
myDisplayer(response.headers.get("content-type"));
}
Try it Yourself »
The above code tells you if the server returned what you expected.
Common Response Headers
| Header | Description |
|---|---|
| Content-Type | Type of the response (text/html, application/json, etc.) |
| Content-Length | Size of the response in bytes |
| Cache-Control | How the response may be cached |
| Expires | When the cached response expires |
| Last-Modified | When the resource was last modified |
| ETag | Resource version identifier used for caching |
| Date | Time the server generated the response |
| Server | Information about the web server |
| Access-Control-Allow-Origin | CORS policy |
| Content-Encoding | Compression used (gzip, br, etc.) |
Use the Browser Network Tab
The Network tab is a panel in your Browser's Developer Tools.
It shows all network request made by your web page.
It is one of the most useful tools for debugging asynchronous JavaScript because it lets you see exactly what is happening when your code uses fetch(), loads images, downloads JSON, or sends data to a server.
How to Use the Developer Tools:
- Open a Website
- Press F12, or
- Right-click the page and select Inspect.
- Then select the Network tab.
- Refresh the page
The Network tab shows every request made by your application:
- Which files were requested
- Whether they succeeded
- HTTP status codes
- Download times
Example
Suppose your code is:
const response = await fetch("customer.json");
Some Possible Results
| Name | Status | Type | Size | Time |
|---|---|---|---|---|
| customer.json | 200 | fetch | 548 Bytes | 134ms |
| customer.json | 404 | fetch | ||
| customer.json | Failed |
This is much easier than trying to guess why fetch() failed.
Use Breakpoints
Modern browsers allow you to pause JavaScript execution.
While execution is paused, you can inspect variables, Promises, and the Call Stack.
Example
const response = await fetch("demo.txt");
debugger;
const text = await response.text();
When execution reaches debugger, DevTools pauses automatically.
Breakpoints on await
You can set breakpoints on await lines.
This lets you inspect values before and after async steps.
Example
async function loadData() {
let response = await fetch("data.json");
let data = await response.json();
console.log(data);
}
Set a breakpoint on the first await line.
Step over and inspect response.
Common Async Errors
Forgetting await is the most common beginner mistake.
Example
async function loadData() {
let response = await fetch("data.json");
let data = response.json();
console.log(data);
}
This logs a promise, not the JSON.
Example
async function loadData() {
let response = await fetch("data.json");
let data = await response.json();
console.log(data);
}
Debugging Promise Chains
Promises can fail anywhere in the chain.
Add logs between steps to find where it fails.
Example
fetch("data.json")
.then(function(response) {
console.log("Got response");
return response.json();
})
.then(function(data) {
console.log("Got data");
console.log(data);
})
.catch(function(error) {
console.log("Failed");
console.log(error);
});
This shows which step is failing.
Understand Execution Order
Example
myDisplayer("Start");
setTimeout(function() {myDisplayer("Timer")},1000);
myDisplayer("End");
Result
Start
End
Timer
Try it Yourself »
Debug Parallel Operations
Result
const customer = fetch("customer.json");
const products = fetch("products.json");
const news = fetch("news.json");
- All three requests start immediately
- The browser may finish them in any order
- That is normal
Check response.ok
Many beginners forget this.
Wrong:
const response = await fetch("missing.txt");
const text = await response.text();
Better:
if (!response.ok) {
console.log(response.status);
}
Handle Promise Rejections
Example
fetch("missing.txt")
.then(function(response){
return response.text();
})
.catch(function(err){
console.log(err);
});
Explain the difference between:
- Rejected Promise
- HTTP error
Use finally()
Example
showSpinner();
try {
...
}
finally {
hideSpinner();
}
Cleanup code belongs in finally().
Debugging HowTo
- Add
try...catcharound awaited code. - Check both
response.okandresponse.status. - Use breakpoints on
awaitlines. - Use the Network tab.
Debugging Checklist
- Did the Promise resolve?
- Did you forget
await? - Did you check
response.ok? - Is your code inside an
asyncfunction? - Did the request appear in the Network tab?
- Are errors reported in the Console?
- Are you blocking the Event Loop?
Summary
- Use
console.log()to trace execution. - Inspect Promise objects while debugging.
- Use
try...catchto handle rejected Promises. - Use the browser's Network tab to inspect requests.
- Set breakpoints to inspect variables and execution.
- Check
response.okafter everyfetch().