In my case, I needed to fill the whole circle, and not just around the perimeter. Using the answer above and setting the line width to double, the radius gave undesirable results, so I wrote my own.
class ArcGradientOptions { constructor(options) { function validateParam(test, errorMessage, fatal = false) { if (!test) { if (fatal) { throw new Error(errorMessage); } else { console.assert(false, errorMessage); } } } options = Object.assign({ useDegrees: false, resolutionFactor: 8, }, options); validateParam( (options.resolutionFactor instanceof Number | typeof options.resolutionFactor === 'number') && options.resolutionFactor > 0, `ArcGradientOptions.resolutionFactor must be a Number greater than 0. Given: ${options.resolutionFactor}`, true); Object.assign(this, options); } }; (function () { CanvasRenderingContext2D.prototype.strokeArcGradient = function (x, y, radius, startAngle, endAngle, colorStops, options) { options = new ArcGradientOptions(options); let lineWidth = this.lineWidth; this.fillArcGradient(x, y, startAngle, endAngle, colorStops, radius + lineWidth / 2, radius - lineWidth / 2, options); } CanvasRenderingContext2D.prototype.fillArcGradient = function (x, y, startAngle, endAngle, colorStops, outerRadius, innerRadius = 0, options) { options = new ArcGradientOptions(options); let oldLineWidth = this.lineWidth, oldStrokeStyle = this.strokeStyle; if (options.useDegrees) { startAngle = startAngle * Math.PI / 180; endAngle = endAngle * Math.PI / 180; } let deltaArcAngle = endAngle - startAngle; gradientWidth = Math.floor(outerRadius * Math.abs(deltaArcAngle) * options.resolutionFactor), gData = generateGradientImgData(gradientWidth, colorStops).data; this.lineWidth = Math.min(4 / options.resolutionFactor, 1); for (let i = 0; i < gradientWidth; i++) { let gradi = i * 4, theta = startAngle + deltaArcAngle * i / gradientWidth; this.strokeStyle = `rgba(${gData[gradi]}, ${gData[gradi + 1]}, ${gData[gradi + 2]}, ${gData[gradi + 3]})`; this.beginPath(); this.moveTo(x + Math.cos(theta) * innerRadius, y + Math.sin(theta) * innerRadius); this.lineTo(x + Math.cos(theta) * outerRadius, y + Math.sin(theta) * outerRadius); this.stroke(); this.closePath(); } this.lineWidth = oldLineWidth; this.strokeStyle = oldStrokeStyle; } function generateGradientImgData(width, colorStops) { let canvas = document.createElement('canvas'); canvas.setAttribute('width', width); canvas.setAttribute('height', 1); let ctx = canvas.getContext('2d'), gradient = ctx.createLinearGradient(0, 0, width, 0); for (let i = 0; i < colorStops.length; i++) { gradient.addColorStop(colorStops[i].offset, colorStops[i].color); } ctx.fillStyle = gradient; ctx.fillRect(0, 0, width, 1); return ctx.getImageData(0, 0, width, 1); } })();
This method draws lines from the center of the circle to each pixel along its edge. This way you get a cleaner gradient.

For large line thicknesses it is even cleaner.

One of its main drawbacks is performance. If your radius is very large, the number of lines needed to create a good circle is about 50 times the radius.
jsFiddle