All posts

Field guide

“jwt.decode is not a function”: the fix (and the security bug behind it)

This error almost always means you mixed up two JWT libraries. The fix is one line. But it is worth a minute, because the same confusion often hides a real security bug: decoding a token when you should be verifying it.

By Prbl Security Team

You called something like jwt.decode(token) or jwt_decode.decode(token) and got TypeError: jwt.decode is not a function. The cause is almost always a mix-up between two different libraries that both deal with JWTs, and the fix is quick. But this error tends to show up right next to a security mistake, so it is worth understanding both.

The quick fix

The jwt-decode library exports the decode function itself, not an object with a .decode() method. In current versions it is a named export:

// jwt-decode (browser): reads the payload, does NOT verify
import { jwtDecode } from "jwt-decode";
const payload = jwtDecode(token);   // not jwtDecode.decode(token)

// jsonwebtoken (server): this one DOES have .decode() and .verify()
import jwt from "jsonwebtoken";
const claims = jwt.verify(token, process.env.JWT_SECRET);

If you were calling .decode(), you were likely using jwt-decode as if it were jsonwebtoken. Pick the one that matches where your code runs: jwt-decode in the browser to read a claim for display, jsonwebtoken on the server to actually trust the token.

Scan your own app for issues like these

Paste your live URL. We check what your app serves publicly for exposed keys and misconfigurations. No account, no install.

If you upgraded jwt-decode to v4: named export breaking change

jwt-decode v4 changed from a default export to a named export. Code that worked in v3 breaks silently or with a TypeError in v4:

// v3 (no longer works in v4)
import jwtDecode from "jwt-decode";
const payload = jwtDecode(token); // TypeError in v4

// v4 — use the named export
import { jwtDecode } from "jwt-decode";
const payload = jwtDecode(token); // ✅

If you cannot upgrade immediately, pin jwt-decode to v3 in package.json: "jwt-decode": "^3.1.2". If you are on v4 and the import still fails, check that your bundler is not caching a stale build — delete .next or node_modules/.cache and rebuild.

Using jose instead (works in Next.js Edge Runtime)

jsonwebtoken uses Node.js-only crypto APIs and does not run in Next.js middleware, Edge Functions, or Cloudflare Workers. jose is the modern alternative that works everywhere:

import { jwtVerify, decodeJwt } from "jose";

// Verify (server/edge) — throws if invalid or expired
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret, {
  algorithms: ["HS256"],
});

// Decode only (browser/display) — no verification
const payload = decodeJwt(token);

Use jwtVerify anywhere you need to trust the token (middleware, API routes, server components). Use decodeJwt only when you are displaying a claim and the server has already verified the session.

The security bug this often hides

Here is why the error is worth pausing on. People usually hit it while trying to read a value out of a token, a user id, a role, an isAdmin flag, to decide what the user is allowed to do. And decoding, with either library, only reads the payload. It does not check that the token is genuine. Anyone can craft a token with any claims they want, so trusting a decoded field for an authorization decision is a real vulnerability.

If the token is being used to make a trust decision, you must verify it on the server, which checks the signature against your secret, not just decode it. The full explanation is in JWT decode vs verify, and the concept behind it is in what a JWT is.

The rule of thumb

  • Reading a claim for display in the browser: jwtDecode(token) from jwt-decode is fine. Never trust the result for access control.
  • Making an authorization decision: jwt.verify(token, secret) from jsonwebtoken, on the server, every time.
  • If you are not sure which you are doing: you are probably decoding when you should be verifying. Assume verify.

Frequently asked questions

Why do I get 'jwt.decode is not a function'?

Almost always because you imported the jwt-decode library as if it were an object with a .decode method, when it exports the decode function itself. In older versions you wrote `import jwt_decode from 'jwt-decode'` and called `jwt_decode(token)`; in v4+ it is a named export, `import { jwtDecode } from 'jwt-decode'`, called as `jwtDecode(token)`. Calling `.decode()` on it fails because there is no such method. The other common cause is confusing jwt-decode with jsonwebtoken, which does have `jwt.decode()` and `jwt.verify()`.

What changed in jwt-decode v4?

In jwt-decode v4, the library switched from a default export to a named export. Code that worked in v3 — `import jwtDecode from 'jwt-decode'; jwtDecode(token)` — breaks in v4 with a TypeError. The fix is to use the named import: `import { jwtDecode } from 'jwt-decode'`. If you cannot upgrade, pin jwt-decode to v3 in package.json.

What's the difference between jwt-decode and jsonwebtoken?

jwt-decode is a tiny browser library that only reads the payload; it cannot verify a signature because that needs your secret, which never belongs in the browser. jsonwebtoken is a server library that can both decode and, importantly, verify. If you are on the server and need to trust the token, use jsonwebtoken's verify. If you are in the browser and only want to read a claim for display, jwt-decode is fine, but never trust what it returns for an access decision.

Which import should I use?

In the browser, `import { jwtDecode } from 'jwt-decode'` and call `jwtDecode(token)` to read the payload for display only. On the server, `import jwt from 'jsonwebtoken'` and call `jwt.verify(token, secret)` to actually trust the token. The error you hit is a signal to check which library you are using and, more importantly, whether you are verifying or just decoding.

Can I use jose instead of jsonwebtoken?

Yes, and jose is often the better choice for Next.js App Router and Edge Runtime, where jsonwebtoken does not run because it uses Node-only crypto APIs. jose works in the browser, Edge Functions, and Node.js. Use `import { jwtVerify } from 'jose'` and `await jwtVerify(token, secret)` — it verifies the signature and returns the payload if valid, or throws if not.

Why does this error matter for security?

Because the fix people reach for often leaves the real bug in place. If you were trying to read a user's role or id from a token to make an authorization decision, decoding it — however you spell the call — is not enough. Anyone can hand you a token with any claims they like. You have to verify the signature on the server before you trust a single field. The error is a good moment to check whether you are decoding when you should be verifying.

Does jwt-decode validate the token signature?

No. jwt-decode intentionally does not validate the signature — it has no verify function and accepts no secret key. It exists only to read the payload for display purposes, after the server has already verified the token. If you need signature validation, use jsonwebtoken (Node.js) or jose (Node.js, Edge, browser).

Check your token handling

Decoding a token instead of verifying it is one of the patterns we look for, because it passes every functional test and only fails when someone forges a token. A scan reads the repo and flags a decode used where a verify belongs. Run a free scan and see whether your app trusts a token it never verified.

Ready to check your own app?

Paste your live URL. We check what your app serves publicly for exposed keys and misconfigurations. No account, no install.

Or see a live example scan first.

"jwt.decode is not a function": The Fix (and the Security Bug Behind It) | Prbl