Async/Await & Error Handling
Every fetch() and axios call in the previous lesson returned a
Promise — a JavaScript object representing a value that isn't available yet,
but will be (or will fail) at some point in the future. async/await is
the standard modern syntax for working with Promises without chaining .then()
calls, and it's used everywhere in real React Native apps. This lesson covers that syntax
properly, how to handle errors around it, how to run multiple requests at once, and how to wire
all of it into a component's UI state.
async/await: The Standard Way to Work With Promises
Marking a function async does two things: it lets you use await inside
it to pause execution until a Promise settles, and it means the function
itself always returns a Promise — even if you write a plain
return statement inside it.
async function getGreeting() {
return 'hello';
}
// getGreeting() does NOT return the string 'hello' directly —
// it returns a Promise that resolves to 'hello'
getGreeting().then(message => console.log(message)); // 'hello'
// inside another async function, await unwraps that Promise for you
async function run() {
const message = await getGreeting();
console.log(message); // 'hello'
}
try/catch: The Standard Error-Handling Pattern
When a Promise rejects — a network request fails, for example — an
await-ed call throws, exactly like a synchronous error. Wrapping your
await calls in a try/catch block is the standard,
idiomatic way to handle that:
async function loadArticles() {
try {
const response = await fetch('https://api.example.com/articles');
if (!response.ok) {
throw new Error(`Server responded with ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// runs for BOTH a network failure (no internet, DNS error)
// AND the manually thrown error above for a bad status
console.error('Failed to load articles:', error.message);
throw error; // re-throw so the caller (e.g. a component) can react too
}
}
Notice the if (!response.ok) check and manual throw inside the
try block — this is the important gotcha carried over from the previous lesson.
fetch()'s Promise only rejects on a genuine network failure; it does
not reject just because the server returned a 404 or 500. Without that manual
check and throw, a failed request would silently be treated as a success, and
catch would never run for it.
Running Requests Concurrently With Promise.all()
If you need data from two independent endpoints — say, a user's profile and that same user's posts — awaiting them one after another wastes time: the second request doesn't start until the first one fully finishes, even though neither depends on the other's result.
// Slower: sequential — the second request waits for the first to fully finish
async function loadProfileSequential(userId) {
const user = await fetch(`https://api.example.com/users/${userId}`).then(r => r.json());
const posts = await fetch(`https://api.example.com/users/${userId}/posts`).then(r => r.json());
return { user, posts };
}
Promise.all() starts every request at the same time and waits for all of them to
finish together, which can be significantly faster whenever the requests don't depend on each
other:
// Faster: both requests fire at the same time
async function loadProfileConcurrent(userId) {
const [userResponse, postsResponse] = await Promise.all([
fetch(`https://api.example.com/users/${userId}`),
fetch(`https://api.example.com/users/${userId}/posts`),
]);
const user = await userResponse.json();
const posts = await postsResponse.json();
return { user, posts };
}
If each request takes roughly 500ms, the sequential version takes around 1 second total (500ms + 500ms, one after the other), while the concurrent version takes around 500ms total, since both requests are in flight over the network at the same time. This gap only grows as you add more independent requests.
Promise.all() rejects as soon as any one of its Promises rejects — so a single failed request fails the whole group. If you need the others to still complete even when one fails, look into Promise.allSettled(), which always waits for every Promise and reports each one's individual outcome.A Full Loading/Error/Data Pattern With useState and useEffect
In a component, the most common pattern for showing fetched data is to track three states —
whether it's still loading, whether it failed, and the data itself — using
useState, and kick off the fetch inside useEffect (both covered in
State & Hooks):
import React, { useState, useEffect } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
function ArticleList() {
const [articles, setArticles] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function load() {
try {
setLoading(true);
setError(null);
const response = await fetch('https://api.example.com/articles');
if (!response.ok) {
throw new Error(`Server responded with ${response.status}`);
}
const data = await response.json();
setArticles(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
load();
}, []);
if (loading) {
return <ActivityIndicator size="large" />;
}
if (error) {
return <Text>Something went wrong: {error}</Text>;
}
return (
<View>
{articles.map(article => (
<Text key={article.id}>{article.title}</Text>
))}
</View>
);
}
This three-state shape — loading, error, and the actual data — is one
of the most common patterns you'll write in any React Native app that talks to a network, and
it's worth internalizing: it makes exactly what the UI should render at any given moment
unambiguous.
A Brief Note on Error Boundaries
Everything above handles errors from async code — failed requests, rejected Promises.
But a completely different category of error can happen: a bug during
rendering itself (for example, reading a property off undefined
somewhere in your JSX), which try/catch around a fetch call can't
protect against. React's answer to that is an Error Boundary — a special
component that catches rendering errors in its child tree and shows a fallback UI instead of
crashing the whole app. Implementing one is outside the scope of this lesson, but it's worth
knowing the concept exists: many production apps wrap their root navigator in one, so a single
unexpected rendering bug in a screen doesn't take down the entire app.