Modules, Async Code, and Error Handling
Structure modules, type asynchronous functions, work with promises, and handle unknown errors safely.
Modules, Imports, and Exports
ES modules use export and import to share values and types. Use import type or export type when importing or exporting only types, especially when compiler settings preserve module syntax or your build system benefits from clear type-only imports. Type-only imports are erased from runtime output.
Module resolution is environment-sensitive. Node ESM, CommonJS, and bundler-based frontend apps have different expectations for extensions, package exports, and resolution. Follow the target platform's guidance and avoid mixing module systems casually.
// domain.ts
export type User = { id: string; name: string };
export const defaultRole = "member";
// app.ts
import { defaultRole, type User } from "./domain.js";
const user: User = { id: "u-1", name: "Mira" };
console.log(defaultRole, user.name);User import is used only for type checking, while defaultRole is a runtime value. The .js extension shown is appropriate in many NodeNext ESM source projects because emitted JavaScript will use that extension.- Use modules to establish clear boundaries.
- Type-only imports clarify compile-time-only dependencies.
- Match import specifiers to the project's module system.
What is the purpose of `import type`?
Promises and async/await
An async function returns a Promise of its resolved return type. If the function returns a User, its type is typically Promise<User>. await unwraps the promise result inside an async function, while thrown errors reject the returned promise.
Type the resolved value rather than annotating every intermediate expression unnecessarily. Use Promise<void> when asynchronous work has no meaningful result. Remember that TypeScript checks the declared types but cannot guarantee that a remote server actually returns the promised data shape.
type User = { id: string; name: string };
async function loadUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error("Could not load user");
return response.json() as Promise<User>;
}User. The assertion on response.json() is not runtime validation; in production, validate the decoded JSON before trusting it.- An async function always returns a Promise.
- Promise<T> describes the resolved value type.
- Network responses require runtime validation despite static annotations.
What is the return type of an async function declared `async function f(): Promise<number>`?
Unknown Errors and Result Types
In a catch clause, treat the caught value as unknown: JavaScript permits throwing strings, objects, and other values, not only Error instances. Narrow the value before reading message. For expected failures, a discriminated Result type can make success and failure explicit and force callers to handle both.
Exceptions remain useful for exceptional or infrastructure failures. A Result union can be better for expected domain outcomes such as validation failures. Choose one convention per boundary and document how callers should handle failures.
try {
await saveDocument();
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error("Unknown failure", error);
}
}instanceof Error check narrows the unknown value before accessing message. A catch handler should not assume all thrown values are standard Error instances.- Catch values should be treated as unknown.
- Narrow before accessing error-specific properties.
- Use Result unions for expected failures when explicit handling helps.
Why should a caught value be treated as `unknown`?